diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index 5cfc054..2e81295 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ export_presets.cfg .mono/ data_*/ addons/godot-firebase/.env -*.DS_Store \ No newline at end of file +*.DS_Store +.godot/ +*.tmp diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd new file mode 100644 index 0000000..13f4101 --- /dev/null +++ b/addons/godot-firebase/Utilities.gd @@ -0,0 +1,345 @@ +extends Node +class_name Utilities + +static func get_json_data(value): + if value is PackedByteArray: + value = value.get_string_from_utf8() + var json = JSON.new() + var json_parse_result = json.parse(value) + if json_parse_result == OK: + return json.data + + return null + + +# Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields +# Field Path3D using the "dot" (`.`) notation are supported: +# ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } } +static func dict2fields(dict : Dictionary) -> Dictionary: + var fields = {} + var var_type : String = "" + for field in dict.keys(): + var field_value = dict[field] + if field is String and "." in field: + var keys: Array = field.split(".") + field = keys.pop_front() + keys.reverse() + for key in keys: + field_value = { key : field_value } + + match typeof(field_value): + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_FLOAT: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_DICTIONARY: + if is_field_timestamp(field_value): + var_type = "timestampValue" + field_value = dict2timestamp(field_value) + else: + var_type = "mapValue" + field_value = dict2fields(field_value) + TYPE_ARRAY: + var_type = "arrayValue" + field_value = {"values": array2fields(field_value)} + + if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"): + for key in field_value["fields"].keys(): + fields[field]["mapValue"]["fields"][key] = field_value["fields"][key] + else: + fields[field] = { var_type : field_value } + + return {'fields' : fields} + +class FirebaseTypeConverter extends RefCounted: + var converters = { + "nullValue": _to_null, + "booleanValue": _to_bool, + "integerValue": _to_int, + "doubleValue": _to_float + } + + func convert_value(type, value): + if converters.has(type): + return converters[type].call(value) + + return value + + func _to_null(value): + return null + + func _to_bool(value): + return bool(value) + + func _to_int(value): + return int(value) + + func _to_float(value): + return float(value) + +static func from_firebase_type(value): + if value == null: + return null + + if value.has("mapValue"): + value = fields2dict(value.values()[0]) + elif value.has("arrayValue"): + value = fields2array(value.values()[0]) + elif value.has("timestampValue"): + value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) + else: + var converter = FirebaseTypeConverter.new() + value = converter.convert_value(value.keys()[0], value.values()[0]) + + return value + + +static func to_firebase_type(value : Variant) -> Dictionary: + var var_type : String = "" + + match typeof(value): + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_FLOAT: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_DICTIONARY: + if is_field_timestamp(value): + var_type = "timestampValue" + value = dict2timestamp(value) + else: + var_type = "mapValue" + value = dict2fields(value) + TYPE_ARRAY: + var_type = "arrayValue" + value = {"values": array2fields(value)} + + return { var_type : value } + +# Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' } +static func fields2dict(doc) -> Dictionary: + var dict = {} + if doc.has("fields"): + var fields = doc["fields"] + + for field in fields.keys(): + if fields[field].has("mapValue"): + dict[field] = (fields2dict(fields[field].mapValue)) + elif fields[field].has("timestampValue"): + dict[field] = timestamp2dict(fields[field].timestampValue) + elif fields[field].has("arrayValue"): + dict[field] = fields2array(fields[field].arrayValue) + elif fields[field].has("integerValue"): + dict[field] = fields[field].values()[0] as int + elif fields[field].has("doubleValue"): + dict[field] = fields[field].values()[0] as float + elif fields[field].has("booleanValue"): + dict[field] = fields[field].values()[0] as bool + elif fields[field].has("nullValue"): + dict[field] = null + else: + dict[field] = fields[field].values()[0] + return dict + +# Pass an Array to parse it to a Firebase arrayValue +static func array2fields(array : Array) -> Array: + var fields : Array = [] + var var_type : String = "" + for field in array: + match typeof(field): + TYPE_DICTIONARY: + if is_field_timestamp(field): + var_type = "timestampValue" + field = dict2timestamp(field) + else: + var_type = "mapValue" + field = dict2fields(field) + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_FLOAT: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_ARRAY: var_type = "arrayValue" + _: var_type = "FieldTransform" + fields.append({ var_type : field }) + return fields + +# Pass a Firebase arrayValue Dictionary to convert it back to an Array +static func fields2array(array : Dictionary) -> Array: + var fields : Array = [] + if array.has("values"): + for field in array.values: + var item + match field.keys()[0]: + "mapValue": + item = fields2dict(field.mapValue) + "arrayValue": + item = fields2array(field.arrayValue) + "integerValue": + item = field.values()[0] as int + "doubleValue": + item = field.values()[0] as float + "booleanValue": + item = field.values()[0] as bool + "timestampValue": + item = timestamp2dict(field.timestampValue) + "nullValue": + item = null + _: + item = field.values()[0] + fields.append(item) + return fields + +# Converts a gdscript Dictionary (most likely obtained with Time.get_datetime_dict_from_system()) to a Firebase Timestamp +static func dict2timestamp(dict : Dictionary) -> String: + #dict.erase('weekday') + #dict.erase('dst') + #var dict_values : Array = dict.values() + var time = Time.get_datetime_string_from_datetime_dict(dict, false) + return time + #return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values + +# Converts a Firebase Timestamp back to a gdscript Dictionary +static func timestamp2dict(timestamp : String) -> Dictionary: + return Time.get_datetime_dict_from_datetime_string(timestamp, false) + #var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} + #var dict : PackedStringArray = timestamp.split("T")[0].split("-") + #dict.append_array(timestamp.split("T")[1].split(":")) + #for value in dict.size(): + #datetime[datetime.keys()[value]] = int(dict[value]) + #return datetime + +static func is_field_timestamp(field : Dictionary) -> bool: + return field.has_all(['year','month','day','hour','minute','second']) + + +# HTTPRequeust seems to have an issue in Web exports where the body returns empty +# This appears to be caused by the gzip compression being unsupported, so we +# disable it when web export is detected. +static func fix_http_request(http_request): + if is_web(): + http_request.accept_gzip = false + +static func is_web() -> bool: + return OS.get_name() in ["HTML5", "Web"] + + +class MultiSignal extends RefCounted: + signal completed(with_signal) + signal all_completed() + + var _has_signaled := false + var _early_exit := false + + var signal_count := 0 + + func _init(sigs : Array[Signal], early_exit := true, should_oneshot := true) -> void: + _early_exit = early_exit + for sig in sigs: + add_signal(sig, should_oneshot) + + func add_signal(sig : Signal, should_oneshot) -> void: + signal_count += 1 + sig.connect( + func(): + if not _has_signaled and _early_exit: + completed.emit(sig) + _has_signaled = true + elif not _early_exit: + completed.emit(sig) + signal_count -= 1 + if signal_count <= 0: # Not sure how it could be less than + all_completed.emit() + , CONNECT_ONE_SHOT if should_oneshot else CONNECT_REFERENCE_COUNTED + ) + +class SignalReducer extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object. + signal completed + + var awaiters : Array[Signal] = [] + + var reducers = { + 0 : func(): completed.emit(), + 1 : func(p): completed.emit(), + 2 : func(p1, p2): completed.emit(), + 3 : func(p1, p2, p3): completed.emit(), + 4 : func(p1, p2, p3, p4): completed.emit() + } + + func add_signal(sig : Signal, param_count : int = 0) -> void: + assert(param_count < 5, "Too many parameters to reduce, just add more!") + sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing + +class SignalReducerWithResult extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object. + signal completed(result) + + var awaiters : Array[Signal] = [] + + var reducers = { + 0 : func(): completed.emit(), + 1 : func(p): completed.emit({1 : p}), + 2 : func(p1, p2): completed.emit({ 1 : p1, 2 : p2 }), + 3 : func(p1, p2, p3): completed.emit({ 1 : p1, 2 : p2, 3 : p3 }), + 4 : func(p1, p2, p3, p4): completed.emit({ 1 : p1, 2 : p2, 3 : p3, 4 : p4 }) + } + + func add_signal(sig : Signal, param_count : int = 0) -> void: + assert(param_count < 5, "Too many parameters to reduce, just add more!") + sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing + +class ObservableDictionary extends RefCounted: + signal keys_changed() + + var _internal : Dictionary + var is_notifying := true + + func _init(copy : Dictionary = {}) -> void: + _internal = copy + + func add(key : Variant, value : Variant) -> void: + _internal[key] = value + if is_notifying: + keys_changed.emit() + + func update(key : Variant, value : Variant) -> void: + _internal[key] = value + if is_notifying: + keys_changed.emit() + + func has(key : Variant) -> bool: + return _internal.has(key) + + func keys(): + return _internal.keys() + + func values(): + return _internal.values() + + func erase(key : Variant) -> bool: + var result = _internal.erase(key) + if is_notifying: + keys_changed.emit() + + return result + + func get_value(key : Variant) -> Variant: + return _internal[key] + + func _get(property: StringName) -> Variant: + if _internal.has(property): + return _internal[property] + + return false + + func _set(property: StringName, value: Variant) -> bool: + update(property, value) + return true + +class AwaitDetachable extends Node2D: + var awaiter : Signal + + func _init(freeable_node, await_signal : Signal) -> void: + awaiter = await_signal + add_child(freeable_node) + awaiter.connect(queue_free) diff --git a/addons/godot-firebase/Utilities.gd.uid b/addons/godot-firebase/Utilities.gd.uid new file mode 100644 index 0000000..fadddb7 --- /dev/null +++ b/addons/godot-firebase/Utilities.gd.uid @@ -0,0 +1 @@ +uid://dgmbytds3kkmh diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 549d7de..ec6c24c 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -2,7 +2,7 @@ ## @meta-version 2.5 ## The authentication API for Firebase. ## Documentation TODO. -tool +@tool class_name FirebaseAuth extends HTTPRequest @@ -50,93 +50,93 @@ var is_busy : bool = false var has_child : bool = false -var tcp_server : TCP_Server = TCP_Server.new() +var tcp_server : TCPServer = TCPServer.new() var tcp_timer : Timer = Timer.new() var tcp_timeout : float = 0.5 -var _headers : PoolStringArray = [ - "Content-Type: application/json", - "Accept: application/json", +var _headers : PackedStringArray = [ + "Content-Type: application/json", + "Accept: application/json", ] var requesting : int = -1 enum Requests { - NONE = -1, - EXCHANGE_TOKEN, - LOGIN_WITH_OAUTH + NONE = -1, + EXCHANGE_TOKEN, + LOGIN_WITH_OAUTH } var auth_request_type : int = -1 enum Auth_Type { - NONE = -1 - LOGIN_EP, - LOGIN_ANON, - LOGIN_CT, - LOGIN_OAUTH, - SIGNUP_EP + NONE = -1, + LOGIN_EP, + LOGIN_ANON, + LOGIN_CT, + LOGIN_OAUTH, + SIGNUP_EP } var _login_request_body : Dictionary = { - "email":"", - "password":"", - "returnSecureToken": true, + "email":"", + "password":"", + "returnSecureToken": true, } var _oauth_login_request_body : Dictionary = { - "postBody":"", - "requestUri":"", - "returnIdpCredential":false, - "returnSecureToken":true + "postBody":"", + "requestUri":"", + "returnIdpCredential":false, + "returnSecureToken":true } var _anonymous_login_request_body : Dictionary = { - "returnSecureToken":true + "returnSecureToken":true } var _refresh_request_body : Dictionary = { - "grant_type":"refresh_token", - "refresh_token":"", + "grant_type":"refresh_token", + "refresh_token":"", } var _custom_token_body : Dictionary = { - "token":"", - "returnSecureToken":true + "token":"", + "returnSecureToken":true } var _password_reset_body : Dictionary = { - "requestType":"password_reset", - "email":"", + "requestType":"password_reset", + "email":"", } var _change_email_body : Dictionary = { - "idToken":"", - "email":"", - "returnSecureToken": true, + "idToken":"", + "email":"", + "returnSecureToken": true, } var _change_password_body : Dictionary = { - "idToken":"", - "password":"", - "returnSecureToken": true, + "idToken":"", + "password":"", + "returnSecureToken": true, } var _account_verification_body : Dictionary = { - "requestType":"verify_email", - "idToken":"", + "requestType":"verify_email", + "idToken":"", } var _update_profile_body : Dictionary = { - "idToken":"", - "displayName":"", - "photoUrl":"", - "deleteAttribute":"", - "returnSecureToken":true + "idToken":"", + "displayName":"", + "photoUrl":"", + "deleteAttribute":"", + "returnSecureToken":true } var _local_port : int = 8060 @@ -144,75 +144,81 @@ var _local_uri : String = "http://localhost:%s/"%_local_port var _local_provider : AuthProvider = AuthProvider.new() func _ready() -> void: - tcp_timer.wait_time = tcp_timeout - tcp_timer.connect("timeout", self, "_tcp_stream_timer") - - if OS.get_name() == "HTML5": - _local_uri += "tmp_js_export.html" + tcp_timer.wait_time = tcp_timeout + tcp_timer.timeout.connect(_tcp_stream_timer) + + Utilities.fix_http_request(self) + if Utilities.is_web(): + _local_uri += "tmp_js_export.html" # Sets the configuration needed for the plugin to talk to Firebase # These settings come from the Firebase.gd script automatically func _set_config(config_json : Dictionary) -> void: - _config = config_json - _signup_request_url %= _config.apiKey - _signin_request_url %= _config.apiKey - _signin_custom_token_url %= _config.apiKey - _signin_with_oauth_request_url %= _config.apiKey - _userdata_request_url %= _config.apiKey - _refresh_request_url %= _config.apiKey - _oobcode_request_url %= _config.apiKey - _delete_account_request_url %= _config.apiKey - _update_account_request_url %= _config.apiKey + _config = config_json + _signup_request_url %= _config.apiKey + _signin_request_url %= _config.apiKey + _signin_custom_token_url %= _config.apiKey + _signin_with_oauth_request_url %= _config.apiKey + _userdata_request_url %= _config.apiKey + _refresh_request_url %= _config.apiKey + _oobcode_request_url %= _config.apiKey + _delete_account_request_url %= _config.apiKey + _update_account_request_url %= _config.apiKey - connect("request_completed", self, "_on_FirebaseAuth_request_completed") - _check_emulating() + request_completed.connect(_on_FirebaseAuth_request_completed) + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION }) - _refresh_request_base_url = "https://securetoken.googleapis.com" - else: - var port : String = _config.emulators.ports.authentication - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Authentication has not been configured.") - else: - _base_url = "http://localhost:{port}/identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION ,port = port }) - _refresh_request_base_url = "http://localhost:{port}/securetoken.googleapis.com".format({port = port}) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION }) + _refresh_request_base_url = "https://securetoken.googleapis.com" + else: + var port : String = _config.emulators.ports.authentication + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Authentication has not been configured.") + else: + _base_url = "http://localhost:{port}/identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION ,port = port }) + _refresh_request_base_url = "http://localhost:{port}/securetoken.googleapis.com".format({port = port}) # Function is used to check if the auth script is ready to process a request. Returns true if it is not currently processing # If false it will print an error func _is_ready() -> bool: - if is_busy: - Firebase._printerr("Firebase Auth is currently busy and cannot process this request") - return false - else: - return true + if is_busy: + Firebase._printerr("Firebase Auth is currently busy and cannot process this request") + return false + else: + return true # Function cleans the URI and replaces spaces with %20 # As of right now we only replace spaces -# We may need to decide to use the percent_encode() String function +# We may need to decide to use the uri_encode() String function func _clean_url(_url): - _url = _url.replace(' ','%20') - return _url + _url = _url.replace(' ','%20') + return _url # Synchronous call to check if any user is already logged in. func is_logged_in() -> bool: - return auth != null and auth.has("idtoken") + return auth != null and auth.has("idtoken") # Called with Firebase.Auth.signup_with_email_and_password(email, password) # You must pass in the email and password to this function for it to work correctly func signup_with_email_and_password(email : String, password : String) -> void: - if _is_ready(): - is_busy = true - _login_request_body.email = email - _login_request_body.password = password - auth_request_type = Auth_Type.SIGNUP_EP - request(_base_url + _signup_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_login_request_body)) + if _is_ready(): + is_busy = true + _login_request_body.email = email + _login_request_body.password = password + auth_request_type = Auth_Type.SIGNUP_EP + var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) + _login_request_body.email = "" + _login_request_body.password = "" + if err != OK: + is_busy = false + Firebase._printerr("Error signing up with password and email: %s" % err) # Called with Firebase.Auth.anonymous_login() @@ -220,395 +226,449 @@ func signup_with_email_and_password(email : String, password : String) -> void: # The response contains the Firebase ID token and refresh token associated with the anonymous user. # The 'mail' field will be empty since no email is linked to an anonymous user func login_anonymous() -> void: - if _is_ready(): - is_busy = true - auth_request_type = Auth_Type.LOGIN_ANON - request(_base_url + _signup_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_anonymous_login_request_body)) - + if _is_ready(): + is_busy = true + auth_request_type = Auth_Type.LOGIN_ANON + var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_anonymous_login_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error logging in as anonymous: %s" % err) # Called with Firebase.Auth.login_with_email_and_password(email, password) # You must pass in the email and password to this function for it to work correctly # If the login fails it will return an error code through the function _on_FirebaseAuth_request_completed func login_with_email_and_password(email : String, password : String) -> void: - if _is_ready(): - is_busy = true - _login_request_body.email = email - _login_request_body.password = password - auth_request_type = Auth_Type.LOGIN_EP - request(_base_url + _signin_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_login_request_body)) + if _is_ready(): + is_busy = true + _login_request_body.email = email + _login_request_body.password = password + auth_request_type = Auth_Type.LOGIN_EP + var err = request(_base_url + _signin_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) + _login_request_body.email = "" + _login_request_body.password = "" + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with password and email: %s" % err) # Login with a custom valid token # The token needs to be generated using an external service/function func login_with_custom_token(token : String) -> void: - if _is_ready(): - is_busy = true - _custom_token_body.token = token - auth_request_type = Auth_Type.LOGIN_CT - request(_base_url + _signin_custom_token_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_custom_token_body)) - + if _is_ready(): + is_busy = true + _custom_token_body.token = token + auth_request_type = Auth_Type.LOGIN_CT + var err = request(_base_url + _signin_custom_token_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_custom_token_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with custom token: %s" % err) # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** the generated token will be automatically captured and a login request will be made if the token is correct func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port : int = _local_port): - get_auth_with_redirect(provider) - yield(get_tree().create_timer(0.5),"timeout") - if has_child == false: - add_child(tcp_timer) - has_child = true - tcp_timer.start() - tcp_server.listen(port, "*") + get_auth_with_redirect(provider) + await get_tree().create_timer(0.5).timeout + if has_child == false: + add_child(tcp_timer) + has_child = true + tcp_timer.start() + tcp_server.listen(port, "*") func get_auth_with_redirect(provider: AuthProvider) -> void: - var url_endpoint: String = provider.redirect_uri - for key in provider.params.keys(): - url_endpoint+=key+"="+provider.params[key]+"&" - url_endpoint += provider.params.redirect_type+"="+_local_uri - url_endpoint = _clean_url(url_endpoint) - if OS.get_name() == "HTML5" and OS.has_feature("JavaScript"): - JavaScript.eval('window.location.replace("' + url_endpoint + '")') - elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": - #in app for ios if the iOS plugin exists - set_local_provider(provider) - Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint) - else: - set_local_provider(provider) - OS.shell_open(url_endpoint) - print(url_endpoint) + var url_endpoint: String = provider.redirect_uri + for key in provider.params.keys(): + url_endpoint+=key+"="+provider.params[key]+"&" + url_endpoint += provider.params.redirect_type+"="+_local_uri + url_endpoint = _clean_url(url_endpoint) + if Utilities.is_web() and OS.has_feature("JavaScript"): + JavaScriptBridge.eval('window.location.replace("' + url_endpoint + '")') + elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": + #in app for ios if the iOS plugin exists + set_local_provider(provider) + Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint) + else: + set_local_provider(provider) + OS.shell_open(url_endpoint) + print(url_endpoint) # Login with Google oAuth2. # A token is automatically obtained using an authorization code using @get_google_auth() # @provider_id and @request_uri can be changed func login_with_oauth(_token: String, provider: AuthProvider) -> void: - var token : String = _token.percent_decode() - print(token) - var is_successful: bool = true - if provider.should_exchange: - exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) - is_successful = yield(self, "token_exchanged") - token = auth.accesstoken - if is_successful and _is_ready(): - is_busy = true - _oauth_login_request_body.postBody = "access_token="+token+"&providerId="+provider.provider_id - _oauth_login_request_body.requestUri = _local_uri - requesting = Requests.LOGIN_WITH_OAUTH - auth_request_type = Auth_Type.LOGIN_OAUTH - request(_base_url + _signin_with_oauth_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_oauth_login_request_body)) - - + if _token: + var token : String = _token.uri_decode() + print(token) + var is_successful: bool = true + if provider.should_exchange: + exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) + is_successful = await self.token_exchanged + token = auth.accesstoken + if is_successful and _is_ready(): + is_busy = true + _oauth_login_request_body.postBody = "access_token="+token+"&providerId="+provider.provider_id + _oauth_login_request_body.requestUri = _local_uri + requesting = Requests.LOGIN_WITH_OAUTH + auth_request_type = Auth_Type.LOGIN_OAUTH + var err = request(_base_url + _signin_with_oauth_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_oauth_login_request_body)) + _oauth_login_request_body.postBody = "" + _oauth_login_request_body.requestUri = "" + if err != OK: + is_busy = false + Firebase._printerr("Error logging in with oauth: %s" % err) # Exchange the authorization oAuth2 code obtained from browser with a proper access id_token func exchange_token(code : String, redirect_uri : String, request_url: String, _client_id: String, _client_secret: String) -> void: - if _is_ready(): - is_busy = true - var exchange_token_body : Dictionary = { - code = code, - redirect_uri = redirect_uri, - client_id = _client_id, - client_secret = _client_secret, - grant_type = "authorization_code", - } - requesting = Requests.EXCHANGE_TOKEN - request(request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(exchange_token_body)) - - + if _is_ready(): + is_busy = true + var exchange_token_body : Dictionary = { + code = code, + redirect_uri = redirect_uri, + client_id = _client_id, + client_secret = _client_secret, + grant_type = "authorization_code", + } + requesting = Requests.EXCHANGE_TOKEN + var err = request(request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(exchange_token_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error exchanging tokens: %s" % err) # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** with this method, the authorization process will be copy-pasted func get_google_auth_manual(provider: AuthProvider = _local_provider) -> void: - provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" - get_auth_with_redirect(provider) + provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" + get_auth_with_redirect(provider) - -# A timer used to listen through TCP on the redirect uri of the request +# A timer used to listen through TCP checked the redirect uri of the request func _tcp_stream_timer() -> void: - var peer : StreamPeer = tcp_server.take_connection() - if peer != null: - var raw_result : String = peer.get_utf8_string(400) - if raw_result != "" and raw_result.begins_with("GET"): - tcp_timer.stop() - remove_child(tcp_timer) - has_child = false - var token : String = "" - for value in raw_result.split(" ")[1].lstrip("/?").split("&"): - var splitted: PoolStringArray = value.split("=") - if _local_provider.params.response_type in splitted[0]: - token = splitted[1] - break - if token == "": - emit_signal("login_failed") - peer.disconnect_from_host() - tcp_server.stop() - var data : PoolByteArray = '
🔥 You can close this window now. 🔥
'.to_ascii() - peer.put_data(("HTTP/1.1 200 OK\n").to_ascii()) - peer.put_data(("Server: Godot Firebase SDK\n").to_ascii()) - peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii()) - peer.put_data("Connection: close\n".to_ascii()) - peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii()) - peer.put_data(data) - login_with_oauth(token, _local_provider) - yield(self, "login_succeeded") - peer.disconnect_from_host() - tcp_server.stop() - - - - -# Function used to logout of the system, this will also remove the local encrypted auth file if there is one + var peer : StreamPeer = tcp_server.take_connection() + if peer != null: + var raw_result : String = peer.get_utf8_string(441) + if raw_result != "" and raw_result.begins_with("GET"): + tcp_timer.stop() + remove_child(tcp_timer) + has_child = false + var token : String = "" + for value in raw_result.split(" ")[1].lstrip("/?").split("&"): + var splitted: PackedStringArray = value.split("=") + if _local_provider.params.response_type in splitted[0]: + token = splitted[1] + break + + if token == "": + login_failed.emit() + peer.disconnect_from_host() + tcp_server.stop() + return + + var data : PackedByteArray = '🔥 You can close this window now. 🔥
'.to_ascii_buffer() + peer.put_data(("HTTP/1.1 200 OK\n").to_ascii_buffer()) + peer.put_data(("Server: Godot Firebase SDK\n").to_ascii_buffer()) + peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii_buffer()) + peer.put_data("Connection: close\n".to_ascii_buffer()) + peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii_buffer()) + peer.put_data(data) + login_with_oauth(token, _local_provider) + await self.login_succeeded + peer.disconnect_from_host() + tcp_server.stop() + + +# Function used to logout of the system, this will also remove_at the local encrypted auth file if there is one func logout() -> void: - auth = {} - remove_auth() - emit_signal("logged_out") + auth = {} + remove_auth() + logged_out.emit() +# Checks to see if we need a hard login +func needs_login() -> bool: + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) + var err = encrypted_file == null + return err # Function is called when requesting a manual token refresh func manual_token_refresh(auth_data): - auth = auth_data - var refresh_token = null - auth = get_clean_keys(auth) - if auth.has("refreshtoken"): - refresh_token = auth.refreshtoken - elif auth.has("refresh_token"): - refresh_token = auth.refresh_token - _needs_refresh = true - _refresh_request_body.refresh_token = refresh_token - request(_refresh_request_base_url + _refresh_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_refresh_request_body)) + auth = auth_data + var refresh_token = null + auth = get_clean_keys(auth) + if auth.has("refreshtoken"): + refresh_token = auth.refreshtoken + elif auth.has("refresh_token"): + refresh_token = auth.refresh_token + _needs_refresh = true + _refresh_request_body.refresh_token = refresh_token + var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error manually refreshing token: %s" % err) # This function is called whenever there is an authentication request to Firebase # On an error, this function with emit the signal 'login_failed' and print the error to the console -func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - print_debug(JSON.parse(body.get_string_from_utf8()).result) - is_busy = false - var res - if response_code == 0: - # Mocked error results to trigger the correct signal. - # Can occur if there is no internet connection, or the service is down, - # in which case there is no json_body (and thus parsing would fail). - res = {"error": { - "code": "Connection error", - "message": "Error connecting to auth service"}} - else: - var bod = body.get_string_from_utf8() - var json_result = JSON.parse(bod) - if json_result.error != OK: - Firebase._printerr("Error while parsing auth body json") - emit_signal("auth_request", ERR_PARSE_ERROR, "Error while parsing auth body json") - return - res = json_result.result - - if response_code == HTTPClient.RESPONSE_OK: - if not res.has("kind"): - auth = get_clean_keys(res) - match requesting: - Requests.EXCHANGE_TOKEN: - emit_signal("token_exchanged", true) - begin_refresh_countdown() - # Refresh token countdown - emit_signal("auth_request", 1, auth) - else: - match res.kind: - RESPONSE_SIGNUP: - auth = get_clean_keys(res) - emit_signal("signup_succeeded", auth) - begin_refresh_countdown() - RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: - auth = get_clean_keys(res) - emit_signal("login_succeeded", auth) - begin_refresh_countdown() - RESPONSE_USERDATA: - var userdata = FirebaseUserData.new(res.users[0]) - emit_signal("userdata_received", userdata) - emit_signal("auth_request", 1, auth) - else: - # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD - if requesting == Requests.EXCHANGE_TOKEN: - emit_signal("token_exchanged", false) - emit_signal("login_failed", res.error, res.error_description) - emit_signal("auth_request", res.error, res.error_description) - else: - var sig = "signup_failed" if auth_request_type == Auth_Type.SIGNUP_EP else "login_failed" - emit_signal(sig, res.error.code, res.error.message) - emit_signal("auth_request", res.error.code, res.error.message) - requesting = Requests.NONE - auth_request_type = Auth_Type.NONE +func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + var json = Utilities.get_json_data(body.get_string_from_utf8()) + is_busy = false + var res + if response_code == 0: + # Mocked error results to trigger the correct signal. + # Can occur if there is no internet connection, or the service is down, + # in which case there is no json_body (and thus parsing would fail). + res = {"error": { + "code": "Connection error", + "message": "Error connecting to auth service"}} + else: + if json == null: + Firebase._printerr("Error while parsing auth body json") + auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json") + return + + res = json + + if response_code == HTTPClient.RESPONSE_OK: + if not res.has("kind"): + auth = get_clean_keys(res) + match requesting: + Requests.EXCHANGE_TOKEN: + token_exchanged.emit(true) + begin_refresh_countdown() + # Refresh token countdown + auth_request.emit(1, auth) + + if _needs_refresh: + _needs_refresh = false + login_succeeded.emit(auth) + else: + match res.kind: + RESPONSE_SIGNUP: + auth = get_clean_keys(res) + signup_succeeded.emit(auth) + begin_refresh_countdown() + RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: + auth = get_clean_keys(res) + login_succeeded.emit(auth) + begin_refresh_countdown() + RESPONSE_USERDATA: + var userdata = FirebaseUserData.new(res.users[0]) + userdata_received.emit(userdata) + auth_request.emit(1, auth) + else: + # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD + if requesting == Requests.EXCHANGE_TOKEN: + token_exchanged.emit(false) + login_failed.emit(res.error, res.error_description) + auth_request.emit(res.error, res.error_description) + else: + var sig = signup_failed if auth_request_type == Auth_Type.SIGNUP_EP else login_failed + sig.emit(res.error.code, res.error.message) + auth_request.emit(res.error.code, res.error.message) + requesting = Requests.NONE + auth_request_type = Auth_Type.NONE + # Function used to save the auth data provided by Firebase into an encrypted file # Note this does not work in HTML5 or UWP -func save_auth(auth : Dictionary) -> void: - var encrypted_file = File.new() - var err = encrypted_file.open_encrypted_with_pass("user://user.auth", File.WRITE, _config.apiKey) - if err != OK: - Firebase._printerr("Error Opening File. Error Code: " + String(err)) - else: - encrypted_file.store_line(to_json(auth)) - encrypted_file.close() +func save_auth(auth : Dictionary) -> bool: + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) + var err = encrypted_file == null + if err: + Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error())) + else: + encrypted_file.store_line(JSON.stringify(auth)) + return not err # Function used to load the auth data file that has been stored locally # Note this does not work in HTML5 or UWP -func load_auth() -> void: - var encrypted_file = File.new() - var err = encrypted_file.open_encrypted_with_pass("user://user.auth", File.READ, _config.apiKey) - if err != OK: - Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(err)) - emit_signal("auth_request", err, "Error Opening Firebase Auth File.") - else: - var encrypted_file_data = parse_json(encrypted_file.get_line()) - manual_token_refresh(encrypted_file_data) - - -# Function used to remove the local encrypted auth file +func load_auth() -> bool: + var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) + var err = encrypted_file == null + if err: + Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(FileAccess.get_open_error())) + auth_request.emit(err, "Error Opening Firebase Auth File.") + else: + var json = JSON.new() + var json_parse_result = json.parse(encrypted_file.get_line()) + if json_parse_result == OK: + var encrypted_file_data = json.data + manual_token_refresh(encrypted_file_data) + return not err + +# Function used to remove_at the local encrypted auth file func remove_auth() -> void: - var dir = Directory.new() - if (dir.file_exists("user://user.auth")): - dir.remove("user://user.auth") - else: - Firebase._printerr("No encrypted auth file exists") + if (FileAccess.file_exists("user://user.auth")): + DirAccess.remove_absolute("user://user.auth") + else: + Firebase._printerr("No encrypted auth file exists") # Function to check if there is an encrypted auth data file # If there is, the game will load it and refresh the token -func check_auth_file() -> void: - var dir = Directory.new() - if (dir.file_exists("user://user.auth")): - # Will ensure "auth_request" emitted - load_auth() - else: - Firebase._printerr("Encrypted Firebase Auth file does not exist") - emit_signal("auth_request", ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") +func check_auth_file() -> bool: + if (FileAccess.file_exists("user://user.auth")): + # Will ensure "auth_request" emitted + return load_auth() + else: + Firebase._printerr("Encrypted Firebase Auth file does not exist") + auth_request.emit(ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") + return false # Function used to change the email account for the currently logged in user func change_user_email(email : String) -> void: - if _is_ready(): - is_busy = true - _change_email_body.email = email - _change_email_body.idToken = auth.idtoken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_change_email_body)) + if _is_ready(): + is_busy = true + _change_email_body.email = email + _change_email_body.idToken = auth.idtoken + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_email_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error changing user email: %s" % err) # Function used to change the password for the currently logged in user func change_user_password(password : String) -> void: - if _is_ready(): - is_busy = true - _change_password_body.password = password - _change_password_body.idToken = auth.idtoken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_change_password_body)) + if _is_ready(): + is_busy = true + _change_password_body.password = password + _change_password_body.idToken = auth.idtoken + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_password_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error changing user password: %s" % err) # User Profile handlers -func update_account(idToken : String, displayName : String, photoUrl : String, deleteAttribute : PoolStringArray, returnSecureToken : bool) -> void: - if _is_ready(): - is_busy = true - _update_profile_body.idToken = idToken - _update_profile_body.displayName = displayName - _update_profile_body.photoUrl = photoUrl - _update_profile_body.deleteAttribute = deleteAttribute - _update_profile_body.returnSecureToken = returnSecureToken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_update_profile_body)) +func update_account(idToken : String, displayName : String, photoUrl : String, deleteAttribute : PackedStringArray, returnSecureToken : bool) -> void: + if _is_ready(): + is_busy = true + _update_profile_body.idToken = idToken + _update_profile_body.displayName = displayName + _update_profile_body.photoUrl = photoUrl + _update_profile_body.deleteAttribute = deleteAttribute + _update_profile_body.returnSecureToken = returnSecureToken + var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_update_profile_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error updating account: %s" % err) # Function to send a account verification email func send_account_verification_email() -> void: - if _is_ready(): - is_busy = true - _account_verification_body.idToken = auth.idtoken - request(_base_url + _oobcode_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_account_verification_body)) + if _is_ready(): + is_busy = true + _account_verification_body.idToken = auth.idtoken + var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_account_verification_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error sending account verification email: %s" % err) # Function used to reset the password for a user who has forgotten in. # This will send the users account an email with a password reset link func send_password_reset_email(email : String) -> void: - if _is_ready(): - is_busy = true - _password_reset_body.email = email - request(_base_url + _oobcode_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_password_reset_body)) + if _is_ready(): + is_busy = true + _password_reset_body.email = email + var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_password_reset_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error sending password reset email: %s" % err) # Function called to get all func get_user_data() -> void: - if _is_ready(): - is_busy = true - if not is_logged_in(): - print_debug("Not logged in") - is_busy = false - return + if _is_ready(): + is_busy = true + if not is_logged_in(): + print_debug("Not logged in") + is_busy = false + return - request(_base_url + _userdata_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print({"idToken":auth.idtoken})) + var err = request(_base_url + _userdata_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) + if err != OK: + is_busy = false + Firebase._printerr("Error getting user data: %s" % err) # Function used to delete the account of the currently authenticated user func delete_user_account() -> void: - if _is_ready(): - is_busy = true - request(_base_url + _delete_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print({"idToken":auth.idtoken})) + if _is_ready(): + is_busy = true + var err = request(_base_url + _delete_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) + if err != OK: + is_busy = false + Firebase._printerr("Error deleting user: %s" % err) + else: + remove_auth() # Function is called when a new token is issued to a user. The function will yield until the token has expired, and then request a new one. func begin_refresh_countdown() -> void: - var refresh_token = null - var expires_in = 1000 - auth = get_clean_keys(auth) - if auth.has("refreshtoken"): - refresh_token = auth.refreshtoken - expires_in = auth.expiresin - elif auth.has("refresh_token"): - refresh_token = auth.refresh_token - expires_in = auth.expires_in - if auth.has("userid"): - auth["localid"] = auth.userid - _needs_refresh = true - emit_signal("token_refresh_succeeded", auth) - yield(get_tree().create_timer(float(expires_in)), "timeout") - _refresh_request_body.refresh_token = refresh_token - request(_refresh_request_base_url + _refresh_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_refresh_request_body)) + var refresh_token = null + var expires_in = 1000 + auth = get_clean_keys(auth) + if auth.has("refreshtoken"): + refresh_token = auth.refreshtoken + expires_in = auth.expiresin + elif auth.has("refresh_token"): + refresh_token = auth.refresh_token + expires_in = auth.expires_in + if auth.has("userid"): + auth["localid"] = auth.userid + _needs_refresh = true + token_refresh_succeeded.emit(auth) + await get_tree().create_timer(float(expires_in)).timeout + _refresh_request_body.refresh_token = refresh_token + var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) + if err != OK: + is_busy = false + Firebase._printerr("Error refreshing via countdown: %s" % err) func get_token_from_url(provider: AuthProvider): - var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" - if OS.has_feature('JavaScript'): - var token = JavaScript.eval(""" - var url_string = window.location.href.replaceAll('?#', '?'); - var url = new URL(url_string); - url.searchParams.get('"""+token_type+"""'); - """) - JavaScript.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") - return token - return null + var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" + if OS.has_feature('web'): + var token = JavaScriptBridge.eval(""" + var url_string = window.location.href.replaceAll('?#', '?'); + var url = new URL(url_string); + url.searchParams.get('"""+token_type+"""'); + """) + JavaScriptBridge.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") + return token + return null func set_redirect_uri(redirect_uri : String) -> void: - self._local_uri = redirect_uri + self._local_uri = redirect_uri func set_local_provider(provider : AuthProvider) -> void: - self._local_provider = provider + self._local_provider = provider # This function is used to make all keys lowercase -# This is only used to cut down on processing errors from Firebase +# This is only used to cut down checked processing errors from Firebase # This is due to Google have inconsistencies in the API that we are trying to fix func get_clean_keys(auth_result : Dictionary) -> Dictionary: - var cleaned = {} - for key in auth_result.keys(): - cleaned[key.replace("_", "").to_lower()] = auth_result[key] - return cleaned + var cleaned = {} + for key in auth_result.keys(): + cleaned[key.replace("_", "").to_lower()] = auth_result[key] + return cleaned # -------------------- # PROVIDERS # -------------------- func get_GoogleProvider() -> GoogleProvider: - return GoogleProvider.new(_config.clientId, _config.clientSecret) + return GoogleProvider.new(_config.clientId, _config.clientSecret) func get_FacebookProvider() -> FacebookProvider: - return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret) + return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret) func get_GitHubProvider() -> GitHubProvider: - return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret) + return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret) func get_TwitterProvider() -> TwitterProvider: - return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret) + return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret) diff --git a/addons/godot-firebase/auth/auth.gd.uid b/addons/godot-firebase/auth/auth.gd.uid new file mode 100644 index 0000000..41579ac --- /dev/null +++ b/addons/godot-firebase/auth/auth.gd.uid @@ -0,0 +1 @@ +uid://huvi02qxa0gf diff --git a/addons/godot-firebase/auth/auth_provider.gd b/addons/godot-firebase/auth/auth_provider.gd index ec0742d..288cf2d 100644 --- a/addons/godot-firebase/auth/auth_provider.gd +++ b/addons/godot-firebase/auth/auth_provider.gd @@ -1,6 +1,6 @@ -tool +@tool class_name AuthProvider -extends Reference +extends RefCounted var redirect_uri: String = "" var access_token_uri: String = "" diff --git a/addons/godot-firebase/auth/auth_provider.gd.uid b/addons/godot-firebase/auth/auth_provider.gd.uid new file mode 100644 index 0000000..46a2b37 --- /dev/null +++ b/addons/godot-firebase/auth/auth_provider.gd.uid @@ -0,0 +1 @@ +uid://bs2akaik8bdp0 diff --git a/addons/godot-firebase/auth/providers/facebook.gd b/addons/godot-firebase/auth/providers/facebook.gd index 32c1290..35e8dd8 100644 --- a/addons/godot-firebase/auth/providers/facebook.gd +++ b/addons/godot-firebase/auth/providers/facebook.gd @@ -1,21 +1,21 @@ class_name FacebookProvider extends AuthProvider -func _init(client_id: String, client_secret: String) -> void: - randomize() - set_client_id(client_id) - set_client_secret(client_secret) - - self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?" - self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token" - self.provider_id = "facebook.com" - self.params.scope = "public_profile" - self.params.state = str(rand_range(0, 1)) - if OS.get_name() == "HTML5": - self.should_exchange = false - self.params.response_type = "token" - else: - self.should_exchange = true - self.params.response_type = "code" - - +func _init(client_id: String,client_secret: String): + randomize() + set_client_id(client_id) + set_client_secret(client_secret) + + self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?" + self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token" + self.provider_id = "facebook.com" + self.params.scope = "public_profile" + self.params.state = str(randf_range(0, 1)) + if Utilities.is_web(): + self.should_exchange = false + self.params.response_type = "token" + else: + self.should_exchange = true + self.params.response_type = "code" + + diff --git a/addons/godot-firebase/auth/providers/facebook.gd.uid b/addons/godot-firebase/auth/providers/facebook.gd.uid new file mode 100644 index 0000000..0735b9a --- /dev/null +++ b/addons/godot-firebase/auth/providers/facebook.gd.uid @@ -0,0 +1 @@ +uid://hkjf41s4b2g2 diff --git a/addons/godot-firebase/auth/providers/github.gd b/addons/godot-firebase/auth/providers/github.gd index 1716258..bab073a 100644 --- a/addons/godot-firebase/auth/providers/github.gd +++ b/addons/godot-firebase/auth/providers/github.gd @@ -1,7 +1,7 @@ class_name GitHubProvider extends AuthProvider -func _init(client_id: String, client_secret: String) -> void: +func _init(client_id: String,client_secret: String): randomize() set_client_id(client_id) set_client_secret(client_secret) @@ -10,5 +10,5 @@ func _init(client_id: String, client_secret: String) -> void: self.access_token_uri = "https://github.com/login/oauth/access_token" self.provider_id = "github.com" self.params.scope = "user:read" - self.params.state = str(rand_range(0, 1)) + self.params.state = str(randf_range(0, 1)) self.params.response_type = "code" diff --git a/addons/godot-firebase/auth/providers/github.gd.uid b/addons/godot-firebase/auth/providers/github.gd.uid new file mode 100644 index 0000000..b74b76a --- /dev/null +++ b/addons/godot-firebase/auth/providers/github.gd.uid @@ -0,0 +1 @@ +uid://c6lelyaje3u04 diff --git a/addons/godot-firebase/auth/providers/google.gd b/addons/godot-firebase/auth/providers/google.gd index 48ad38f..152a5cc 100644 --- a/addons/godot-firebase/auth/providers/google.gd +++ b/addons/godot-firebase/auth/providers/google.gd @@ -1,13 +1,13 @@ class_name GoogleProvider extends AuthProvider -func _init(client_id: String, client_secret: String) -> void: - set_client_id(client_id) - set_client_secret(client_secret) - self.should_exchange = true - self.redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth?" - self.access_token_uri = "https://oauth2.googleapis.com/token" - self.provider_id = "google.com" - self.params.response_type = "code" - self.params.scope = "email openid profile" - self.params.response_type = "code" +func _init(client_id: String,client_secret: String): + set_client_id(client_id) + set_client_secret(client_secret) + self.should_exchange = true + self.redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth?" + self.access_token_uri = "https://oauth2.googleapis.com/token" + self.provider_id = "google.com" + self.params.response_type = "code" + self.params.scope = "email openid profile" + self.params.response_type = "code" diff --git a/addons/godot-firebase/auth/providers/google.gd.uid b/addons/godot-firebase/auth/providers/google.gd.uid new file mode 100644 index 0000000..294ee7f --- /dev/null +++ b/addons/godot-firebase/auth/providers/google.gd.uid @@ -0,0 +1 @@ +uid://c1ymqan4x0rnj diff --git a/addons/godot-firebase/auth/providers/twitter.gd b/addons/godot-firebase/auth/providers/twitter.gd index b4c37b7..1ec11cf 100644 --- a/addons/godot-firebase/auth/providers/twitter.gd +++ b/addons/godot-firebase/auth/providers/twitter.gd @@ -13,14 +13,14 @@ var oauth_header: Dictionary = { oauth_version="1.0" } -func _init(client_id: String, client_secret: String) -> void: +func _init(client_id: String,client_secret: String): randomize() set_client_id(client_id) set_client_secret(client_secret) self.oauth_header.oauth_consumer_key = client_id - self.oauth_header.oauth_nonce = OS.get_ticks_usec() - self.oauth_header.oauth_timestamp = OS.get_ticks_msec() + self.oauth_header.oauth_nonce = Time.get_ticks_usec() + self.oauth_header.oauth_timestamp = Time.get_ticks_msec() self.should_exchange = true @@ -30,10 +30,10 @@ func _init(client_id: String, client_secret: String) -> void: self.params.redirect_type = "redirect_uri" self.params.response_type = "code" self.params.scope = "users.read" - self.params.state = str(rand_range(0, 1)) + self.params.state = str(randf_range(0, 1)) func get_oauth_params() -> String: - var params: PoolStringArray = [] + var params: PackedStringArray = [] for key in self.oauth.keys(): params.append(key+"="+self.oauth.get(key)) - return params.join("&") + return "&".join(params) diff --git a/addons/godot-firebase/auth/providers/twitter.gd.uid b/addons/godot-firebase/auth/providers/twitter.gd.uid new file mode 100644 index 0000000..32f1ab3 --- /dev/null +++ b/addons/godot-firebase/auth/providers/twitter.gd.uid @@ -0,0 +1 @@ +uid://bshm65oo0tmph diff --git a/addons/godot-firebase/auth/user_data.gd b/addons/godot-firebase/auth/user_data.gd index a5ddb85..c76e515 100644 --- a/addons/godot-firebase/auth/user_data.gd +++ b/addons/godot-firebase/auth/user_data.gd @@ -2,9 +2,9 @@ ## @meta-version 2.3 ## Authentication user data. ## Documentation TODO. -tool +@tool class_name FirebaseUserData -extends Reference +extends RefCounted var local_id : String = "" # The uid of the current user. var email : String = "" @@ -18,7 +18,7 @@ var provider_id : String = "" var display_name : String = "" var photo_url : String = "" -func _init(p_userdata : Dictionary) -> void: +func _init(p_userdata : Dictionary): local_id = p_userdata.get("localId", "") email = p_userdata.get("email", "") email_verified = p_userdata.get("emailVerified", false) @@ -27,7 +27,7 @@ func _init(p_userdata : Dictionary) -> void: password_updated_at = float(p_userdata.get("passwordUpdatedAt", 0)) display_name = p_userdata.get("displayName", "") provider_user_info = p_userdata.get("providerUserInfo", []) - if not provider_user_info.empty(): + if not provider_user_info.is_empty(): provider_id = provider_user_info[0].get("providerId", "") photo_url = provider_user_info[0].get("photoUrl", "") display_name = provider_user_info[0].get("displayName", "") diff --git a/addons/godot-firebase/auth/user_data.gd.uid b/addons/godot-firebase/auth/user_data.gd.uid new file mode 100644 index 0000000..210dd3c --- /dev/null +++ b/addons/godot-firebase/auth/user_data.gd.uid @@ -0,0 +1 @@ +uid://cao0vy3osxel5 diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index 1d51b91..3391518 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -2,7 +2,7 @@ ## @meta-version 2.2 ## The Realtime Database API for Firebase. ## Documentation TODO. -tool +@tool class_name FirebaseDatabase extends Node @@ -13,42 +13,39 @@ var _config : Dictionary = {} var _auth : Dictionary = {} func _set_config(config_json : Dictionary) -> void: - _config = config_json - _check_emulating() - + _config = config_json + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = _config.databaseURL - else: - var port : String = _config.emulators.ports.realtimeDatabase - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Realtime Database has not been configured.") - else: - _base_url = "http://localhost" - - + ## Check emulating + if not Firebase.emulating: + _base_url = _config.databaseURL + else: + var port : String = _config.emulators.ports.realtimeDatabase + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Realtime Database has not been configured.") + else: + _base_url = "http://localhost" func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_logout() -> void: - _auth = {} + _auth = {} func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: - var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() - var pusher : HTTPRequest = HTTPRequest.new() - var listener : Node = Node.new() - listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) - var store : FirebaseDatabaseStore = FirebaseDatabaseStore.new() - firebase_reference.set_db_path(path, filter) - firebase_reference.set_auth_and_config(_auth, _config) - firebase_reference.set_pusher(pusher) - firebase_reference.set_listener(listener) - firebase_reference.set_store(store) - add_child(firebase_reference) - return firebase_reference + var firebase_reference = load("res://addons/godot-firebase/database/firebase_database_reference.tscn").instantiate() + firebase_reference.set_db_path(path, filter) + firebase_reference.set_auth_and_config(_auth, _config) + add_child(firebase_reference) + return firebase_reference + +func get_once_database_reference(path : String, filter : Dictionary = {}) -> FirebaseOnceDatabaseReference: + var firebase_reference = load("res://addons/godot-firebase/database/firebase_once_database_reference.tscn").instantiate() + firebase_reference.set_db_path(path, filter) + firebase_reference.set_auth_and_config(_auth, _config) + add_child(firebase_reference) + return firebase_reference diff --git a/addons/godot-firebase/database/database.gd.uid b/addons/godot-firebase/database/database.gd.uid new file mode 100644 index 0000000..f79d7fd --- /dev/null +++ b/addons/godot-firebase/database/database.gd.uid @@ -0,0 +1 @@ +uid://calvdss02mnvk diff --git a/addons/godot-firebase/database/database_store.gd b/addons/godot-firebase/database/database_store.gd index 07b6753..a407241 100644 --- a/addons/godot-firebase/database/database_store.gd +++ b/addons/godot-firebase/database/database_store.gd @@ -1,8 +1,8 @@ ## @meta-authors TODO ## @meta-version 2.2 ## Data structure that holds the currently-known data at a given path (a.k.a. reference) in a Firebase Realtime Database. -## Can process both puts and patches into the data based on realtime events received from the service. -tool +## Can process both puts and patches into the data based checked realtime events received from the service. +@tool class_name FirebaseDatabaseStore extends Node @@ -19,91 +19,91 @@ var _data : Dictionary = { } ## Puts a new payload into this data store at the given path. Any existing values in this data store ## at the specified path will be completely erased. func put(path : String, payload) -> void: - _update_data(path, payload, false) + _update_data(path, payload, false) ## @args path, payload ## Patches an update payload into this data store at the specified path. ## NOTE: When patching in updates to arrays, payload should contain the entire new array! Updating single elements/indexes of an array is not supported. Sometimes when manually mutating array data directly from the Firebase Realtime Database console, single-element patches will be sent out which can cause issues here. func patch(path : String, payload) -> void: - _update_data(path, payload, true) + _update_data(path, payload, true) ## @args path, payload ## Deletes data at the reference point provided ## NOTE: This will delete without warning, so make sure the reference is pointed to the level you want and not the root or you will lose everything func delete(path : String, payload) -> void: - _update_data(path, payload, true) + _update_data(path, payload, true) ## Returns a deep copy of this data store's payload. func get_data() -> Dictionary: - return _data[_ROOT].duplicate(true) + return _data[_ROOT].duplicate(true) # # Updates this data store by either putting or patching the provided payload into it at the given # path. The provided payload can technically be any value. # func _update_data(path: String, payload, patch: bool) -> void: - if debug: - print("Updating data store (patch = %s) (%s = %s)..." % [patch, path, payload]) + if debug: + print("Updating data store (patch = %s) (%s = %s)..." % [patch, path, payload]) - # - # Remove any leading separators. - # - path = path.lstrip(_DELIMITER) + # + # Remove any leading separators. + # + path = path.lstrip(_DELIMITER) - # - # Traverse the path. - # - var dict = _data - var keys = PoolStringArray([_ROOT]) + # + # Traverse the path. + # + var dict = _data + var keys = PackedStringArray([_ROOT]) - keys.append_array(path.split(_DELIMITER, false)) + keys.append_array(path.split(_DELIMITER, false)) - var final_key_idx = (keys.size() - 1) - var final_key = (keys[final_key_idx]) + var final_key_idx = (keys.size() - 1) + var final_key = (keys[final_key_idx]) - keys.remove(final_key_idx) + keys.remove_at(final_key_idx) - for key in keys: - if !dict.has(key): - dict[key] = { } + for key in keys: + if !dict.has(key): + dict[key] = { } - dict = dict[key] + dict = dict[key] - # - # Handle non-patch (a.k.a. put) mode and then update the destination value. - # - var new_type = typeof(payload) + # + # Handle non-patch (a.k.a. put) mode and then update the destination value. + # + var new_type = typeof(payload) - if !patch: - dict.erase(final_key) + if !patch: + dict.erase(final_key) - if new_type == TYPE_NIL: - dict.erase(final_key) - elif new_type == TYPE_DICTIONARY: - if !dict.has(final_key): - dict[final_key] = { } + if new_type == TYPE_NIL: + dict.erase(final_key) + elif new_type == TYPE_DICTIONARY: + if !dict.has(final_key): + dict[final_key] = { } - _update_dictionary(dict[final_key], payload) - else: - dict[final_key] = payload + _update_dictionary(dict[final_key], payload) + else: + dict[final_key] = payload - if debug: - print("...Data store updated (%s)." % _data) + if debug: + print("...Data store updated (%s)." % _data) # # Helper method to "blit" changes in an update dictionary payload onto an original dictionary. # Parameters are directly changed via reference. # func _update_dictionary(original_dict: Dictionary, update_payload: Dictionary) -> void: - for key in update_payload.keys(): - var val_type = typeof(update_payload[key]) - - if val_type == TYPE_NIL: - original_dict.erase(key) - elif val_type == TYPE_DICTIONARY: - if !original_dict.has(key): - original_dict[key] = { } - - _update_dictionary(original_dict[key], update_payload[key]) - else: - original_dict[key] = update_payload[key] + for key in update_payload.keys(): + var val_type = typeof(update_payload[key]) + + if val_type == TYPE_NIL: + original_dict.erase(key) + elif val_type == TYPE_DICTIONARY: + if !original_dict.has(key): + original_dict[key] = { } + + _update_dictionary(original_dict[key], update_payload[key]) + else: + original_dict[key] = update_payload[key] diff --git a/addons/godot-firebase/database/database_store.gd.uid b/addons/godot-firebase/database/database_store.gd.uid new file mode 100644 index 0000000..82e6d81 --- /dev/null +++ b/addons/godot-firebase/database/database_store.gd.uid @@ -0,0 +1 @@ +uid://c5n7o3nahox3j diff --git a/addons/godot-firebase/database/firebase_database_reference.tscn b/addons/godot-firebase/database/firebase_database_reference.tscn new file mode 100644 index 0000000..3a74025 --- /dev/null +++ b/addons/godot-firebase/database/firebase_database_reference.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=5 format=3 uid="uid://btltp52tywbe4"] + +[ext_resource type="Script" uid="uid://cnqrpl3aj03nh" path="res://addons/godot-firebase/database/reference.gd" id="1_l3oy5"] +[ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_0qpk7"] +[ext_resource type="Script" uid="uid://bi48jis3vxlim" path="res://addons/http-sse-client/HTTPSSEClient.gd" id="2_4l0io"] +[ext_resource type="Script" uid="uid://c5n7o3nahox3j" path="res://addons/godot-firebase/database/database_store.gd" id="3_c3r2w"] + +[node name="FirebaseDatabaseReference" type="Node"] +script = ExtResource("1_l3oy5") + +[node name="Pusher" parent="." instance=ExtResource("2_0qpk7")] + +[node name="Listener" type="Node" parent="."] +script = ExtResource("2_4l0io") + +[node name="DataStore" type="Node" parent="."] +script = ExtResource("3_c3r2w") diff --git a/addons/godot-firebase/database/firebase_once_database_reference.tscn b/addons/godot-firebase/database/firebase_once_database_reference.tscn new file mode 100644 index 0000000..c1e2913 --- /dev/null +++ b/addons/godot-firebase/database/firebase_once_database_reference.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=3 format=3 uid="uid://d1u1bxp2fd60e"] + +[ext_resource type="Script" path="res://addons/godot-firebase/database/once_reference.gd" id="1_hq5s2"] +[ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_t2f32"] + +[node name="FirebaseOnceDatabaseReference" type="Node"] +script = ExtResource("1_hq5s2") + +[node name="Pusher" parent="." instance=ExtResource("2_t2f32")] +accept_gzip = false + +[node name="Oncer" parent="." instance=ExtResource("2_t2f32")] +accept_gzip = false + +[connection signal="queue_request_completed" from="Pusher" to="." method="on_push_request_complete"] +[connection signal="queue_request_completed" from="Oncer" to="." method="on_get_request_complete"] diff --git a/addons/godot-firebase/database/once_reference.gd b/addons/godot-firebase/database/once_reference.gd new file mode 100644 index 0000000..ed9f7d0 --- /dev/null +++ b/addons/godot-firebase/database/once_reference.gd @@ -0,0 +1,176 @@ +class_name FirebaseOnceDatabaseReference +extends Node + + +## @meta-authors BackAt50Ft +## @meta-version 1.0 +## A once off reference to a location in the Realtime Database. +## Documentation TODO. + +signal once_successful(dataSnapshot) +signal once_failed() + +signal push_successful() +signal push_failed() + +const ORDER_BY : String = "orderBy" +const LIMIT_TO_FIRST : String = "limitToFirst" +const LIMIT_TO_LAST : String = "limitToLast" +const START_AT : String = "startAt" +const END_AT : String = "endAt" +const EQUAL_TO : String = "equalTo" + +@onready var _oncer = $Oncer +@onready var _pusher = $Pusher + +var _auth : Dictionary +var _config : Dictionary +var _filter_query : Dictionary +var _db_path : String +var _cached_filter : String + +const _separator : String = "/" +const _json_list_tag : String = ".json" +const _query_tag : String = "?" +const _auth_tag : String = "auth=" + +const _auth_variable_begin : String = "[" +const _auth_variable_end : String = "]" +const _filter_tag : String = "&" +const _escaped_quote : String = '"' +const _equal_tag : String = "=" +const _key_filter_tag : String = "$key" + +var _headers : PackedStringArray = [] +var last_etag : String = "" + +func set_db_path(path : String, filter_query_dict : Dictionary) -> void: + _db_path = path + _filter_query = filter_query_dict + +func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void: + _auth = auth_ref + _config = config_ref + +# +# Gets a data snapshot once at the position passed in +# +func once(reference : String) -> void: + last_etag = "" # Reset ETag + var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() + var request_headers = _headers.duplicate() + request_headers.append("X-Firebase-ETag: true") + _oncer.request(ref_pos, request_headers, HTTPClient.METHOD_GET, "") + +func _get_remaining_path(is_push : bool = true) -> String: + var remaining_path = "" + if _filter_query_empty() or is_push: + remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken + else: + remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken + + if Firebase.emulating: + remaining_path += "&ns="+_config.projectId+"-default-rtdb" + + return remaining_path + +func _get_list_url(with_port:bool = true) -> String: + var url = Firebase.Database._base_url.trim_suffix(_separator) + if with_port and Firebase.emulating: + url += ":" + _config.emulators.ports.realtimeDatabase + return url + _separator + + +func _get_filter(): + if _filter_query_empty(): + return "" + # At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules. + if _cached_filter != "": + _cached_filter = "" + if _filter_query.has(ORDER_BY): + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote + _filter_query.erase(ORDER_BY) + else: + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... + for key in _filter_query.keys(): + _cached_filter += _filter_tag + key + _equal_tag + _filter_query[key] + else: + if _filter_query.has(ORDER_BY): + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote + _filter_query.erase(ORDER_BY) + else: + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... + for key in _filter_query.keys(): + _cached_filter += _filter_tag + key + _equal_tag + str(_filter_query[key]) + + return _cached_filter + +func _filter_query_empty() -> bool: + return _filter_query == null or _filter_query.is_empty() + +func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + if response_code == HTTPClient.RESPONSE_OK: + for header in headers: + if header.to_lower().begins_with("etag"): + # Split by first colon only + var parts = header.split(":", true, 1) + if parts.size() > 1: + last_etag = parts[1].strip_edges() + var bod = Utilities.get_json_data(body) + once_successful.emit(bod) + else: + once_failed.emit() + +func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + if response_code == HTTPClient.RESPONSE_OK: + push_successful.emit() + else: + push_failed.emit() + +func push(data : Dictionary) -> void: + var to_push = JSON.stringify(data) + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(true), _headers, HTTPClient.METHOD_POST, to_push) + +func update(path : String, data : Dictionary, etag : String = "") -> void: + path = path.strip_edges(true, true) + + if path == _separator: + path = "" + + var to_update = JSON.stringify(data) + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + + var request_headers = _headers.duplicate() + if etag != "": + request_headers.append("If-Match: %s" % etag) + + _pusher.request(resolved_path, request_headers, HTTPClient.METHOD_PATCH, to_update) + +func put(path : String, data : Dictionary, etag : String = "") -> void: + path = path.strip_edges(true, true) + + if path == _separator: + path = "" + + var to_put = JSON.stringify(data) + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + + var request_headers = _headers.duplicate() + if etag != "": + request_headers.append("If-Match: %s" % etag) + + _pusher.request(resolved_path, request_headers, HTTPClient.METHOD_PUT, to_put) + +func delete(path : String, etag : String = "") -> void: + path = path.strip_edges(true, true) + + if path == _separator: + path = "" + + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + + var request_headers = _headers.duplicate() + if etag != "": + request_headers.append("If-Match: %s" % etag) + + _pusher.request(resolved_path, request_headers, HTTPClient.METHOD_DELETE, "") diff --git a/addons/godot-firebase/database/once_reference.gd.uid b/addons/godot-firebase/database/once_reference.gd.uid new file mode 100644 index 0000000..3c84fd3 --- /dev/null +++ b/addons/godot-firebase/database/once_reference.gd.uid @@ -0,0 +1 @@ +uid://bmk7usmg2abmn diff --git a/addons/godot-firebase/database/reference.gd b/addons/godot-firebase/database/reference.gd index b4ae5c8..4adc864 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -1,8 +1,8 @@ -## @meta-authors TODO -## @meta-version 2.3 +## @meta-authors BackAt50Ft +## @meta-version 2.4 ## A reference to a location in the Realtime Database. ## Documentation TODO. -tool +@tool class_name FirebaseDatabaseReference extends Node @@ -10,9 +10,13 @@ signal new_data_update(data) signal patch_data_update(data) signal delete_data_update(data) +signal once_successful(dataSnapshot) +signal once_failed() + signal push_successful() signal push_failed() + const ORDER_BY : String = "orderBy" const LIMIT_TO_FIRST : String = "limitToFirst" const LIMIT_TO_LAST : String = "limitToLast" @@ -20,17 +24,15 @@ const START_AT : String = "startAt" const END_AT : String = "endAt" const EQUAL_TO : String = "equalTo" -var _pusher : HTTPRequest -var _listener : Node -var _store : FirebaseDatabaseStore +@onready var _pusher := $Pusher +@onready var _listener := $Listener +@onready var _store := $DataStore + var _auth : Dictionary var _config : Dictionary var _filter_query : Dictionary var _db_path : String var _cached_filter : String -var _push_queue : Array = [] -var _update_queue : Array = [] -var _delete_queue : Array = [] var _can_connect_to_host : bool = false const _put_tag : String = "put" @@ -48,153 +50,161 @@ const _escaped_quote : String = '"' const _equal_tag : String = "=" const _key_filter_tag : String = "$key" -var _headers : PoolStringArray = [] +var _headers : PackedStringArray = [] + +func _ready() -> void: +#region Set Listener info + $Listener.new_sse_event.connect(on_new_sse_event) + var base_url = _get_list_url(false).trim_suffix(_separator) + var extended_url = _separator + _db_path + _get_remaining_path(false) + var port = -1 + if Firebase.emulating: + port = int(_config.emulators.ports.realtimeDatabase) + $Listener.connect_to_host(base_url, extended_url, port) +#endregion Set Listener info + +#region Set Pusher info + $Pusher.queue_request_completed.connect(on_push_request_complete) +#endregion Set Pusher info func set_db_path(path : String, filter_query_dict : Dictionary) -> void: - _db_path = path - _filter_query = filter_query_dict + _db_path = path + _filter_query = filter_query_dict func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void: - _auth = auth_ref - _config = config_ref - -func set_pusher(pusher_ref : HTTPRequest) -> void: - if !_pusher: - _pusher = pusher_ref - add_child(_pusher) - _pusher.connect("request_completed", self, "on_push_request_complete") - -func set_listener(listener_ref : Node) -> void: - if !_listener: - _listener = listener_ref - add_child(_listener) - _listener.connect("new_sse_event", self, "on_new_sse_event") - var base_url = _get_list_url(false).trim_suffix(_separator) - var extended_url = _separator + _db_path + _get_remaining_path(false) - var port = -1 - if Firebase.emulating: - port = int(_config.emulators.ports.realtimeDatabase) - _listener.connect_to_host(base_url, extended_url, port) + _auth = auth_ref + _config = config_ref func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) -> void: - if data: - var command = event - if command and command != "keep-alive": - _route_data(command, data.path, data.data) - if command == _put_tag: - if data.path == _separator and data.data and data.data.keys().size() > 0: - for key in data.data.keys(): - emit_signal("new_data_update", FirebaseResource.new(_separator + key, data.data[key])) - elif data.path != _separator: - emit_signal("new_data_update", FirebaseResource.new(data.path, data.data)) - elif command == _patch_tag: - emit_signal("patch_data_update", FirebaseResource.new(data.path, data.data)) - elif command == _delete_tag: - emit_signal("delete_data_update", FirebaseResource.new(data.path, data.data)) - pass - -func set_store(store_ref : FirebaseDatabaseStore) -> void: - if !_store: - _store = store_ref - add_child(_store) - -func update(path : String, data : Dictionary) -> void: - path = path.strip_edges(true, true) - - if path == _separator: - path = "" - - var to_update = JSON.print(data) - var status = _pusher.get_http_client_status() - if status == HTTPClient.STATUS_DISCONNECTED || status != HTTPClient.STATUS_REQUESTING: - var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) - - _pusher.request(resolved_path, _headers, true, HTTPClient.METHOD_PATCH, to_update) - else: - _update_queue.append({"path": path, "data": data}) + if data: + var command = event + if command and command != "keep-alive": + _route_data(command, data.path, data.data) + if command == _put_tag: + if data.path == _separator and data.data and data.data.keys().size() > 0: + for key in data.data.keys(): + new_data_update.emit(FirebaseResource.new(_separator + key, data.data[key])) + elif data.path != _separator: + new_data_update.emit(FirebaseResource.new(data.path, data.data)) + elif command == _patch_tag: + patch_data_update.emit(FirebaseResource.new(data.path, data.data)) + elif command == _delete_tag: + delete_data_update.emit(FirebaseResource.new(data.path, data.data)) + +func update(path : String, data : Dictionary, etag : String = "") -> void: + path = path.strip_edges(true, true) + + if path == _separator: + path = "" + + var to_update = JSON.stringify(data) + + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + + var request_headers = _headers.duplicate() + if etag != "": + request_headers.append("if-match: %s" % etag) + + _pusher.request(resolved_path, request_headers, HTTPClient.METHOD_PATCH, to_update) func push(data : Dictionary) -> void: - var to_push = JSON.print(data) - if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, true, HTTPClient.METHOD_POST, to_push) - else: - _push_queue.append(data) - -func delete(reference : String) -> void: - if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, true, HTTPClient.METHOD_DELETE, "") - else: - _delete_queue.append(reference) + var to_push = JSON.stringify(data) + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) + +func put(path : String, data : Dictionary, etag : String = "") -> void: + path = path.strip_edges(true, true) + + if path == _separator: + path = "" + + var to_put = JSON.stringify(data) + + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + + var request_headers = _headers.duplicate() + if etag != "": + request_headers.append("if-match: %s" % etag) + + _pusher.request(resolved_path, request_headers, HTTPClient.METHOD_PUT, to_put) + +func delete(reference : String, etag : String = "") -> void: + var request_headers = _headers.duplicate() + if etag != "": + request_headers.append("if-match: %s" % etag) + + _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), request_headers, HTTPClient.METHOD_DELETE, "") + # # Returns a deep copy of the current local copy of the data stored at this reference in the Firebase # Realtime Database. # func get_data() -> Dictionary: - if _store == null: - return { } + if _store == null: + return { } - return _store.get_data() + return _store.get_data() func _get_remaining_path(is_push : bool = true) -> String: - var remaining_path = "" - if !_filter_query or is_push: - remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken - else: - remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken + var remaining_path = "" + if _filter_query_empty() or is_push: + remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken + else: + remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken - if Firebase.emulating: - remaining_path += "&ns="+_config.projectId+"-default-rtdb" + if Firebase.emulating: + remaining_path += "&ns="+_config.projectId+"-default-rtdb" - return remaining_path + return remaining_path func _get_list_url(with_port:bool = true) -> String: - var url = Firebase.Database._base_url.trim_suffix(_separator) - if with_port and Firebase.emulating: - url += ":" + _config.emulators.ports.realtimeDatabase - return url + _separator + var url = Firebase.Database._base_url.trim_suffix(_separator) + if with_port and Firebase.emulating: + url += ":" + _config.emulators.ports.realtimeDatabase + return url + _separator func _get_filter(): - if !_filter_query: - return "" - # At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules. - if !_cached_filter: - _cached_filter = "" - if _filter_query.has(ORDER_BY): - _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote - _filter_query.erase(ORDER_BY) - else: - _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... - for key in _filter_query.keys(): - _cached_filter += _filter_tag + key + _equal_tag + _filter_query[key] - - return _cached_filter + if _filter_query_empty(): + return "" + # At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules. + if _cached_filter != "": + _cached_filter = "" + if _filter_query.has(ORDER_BY): + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote + _filter_query.erase(ORDER_BY) + else: + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... + for key in _filter_query.keys(): + _cached_filter += _filter_tag + key + _equal_tag + _filter_query[key] + else: + if _filter_query.has(ORDER_BY): + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote + _filter_query.erase(ORDER_BY) + else: + _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... + for key in _filter_query.keys(): + _cached_filter += _filter_tag + key + _equal_tag + str(_filter_query[key]) + + return _cached_filter + +func _filter_query_empty() -> bool: + return _filter_query == null or _filter_query.is_empty() # # Appropriately updates the current local copy of the data stored at this reference in the Firebase # Realtime Database. # func _route_data(command : String, path : String, data) -> void: - if command == _put_tag: - _store.put(path, data) - elif command == _patch_tag: - _store.patch(path, data) - elif command == _delete_tag: - _store.delete(path, data) - -func on_push_request_complete(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - if response_code == HTTPClient.RESPONSE_OK: - emit_signal("push_successful") - else: - emit_signal("push_failed") - - if _push_queue.size() > 0: - push(_push_queue.pop_front()) - return - if _update_queue.size() > 0: - var e = _update_queue.pop_front() - update(e['path'], e['data']) - return - if _delete_queue.size() > 0: - delete(_delete_queue.pop_front()) + if command == _put_tag: + _store.put(path, data) + elif command == _patch_tag: + _store.patch(path, data) + elif command == _delete_tag: + _store.delete(path, data) + +func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + if response_code == HTTPClient.RESPONSE_OK: + push_successful.emit() + else: + push_failed.emit() diff --git a/addons/godot-firebase/database/reference.gd.uid b/addons/godot-firebase/database/reference.gd.uid new file mode 100644 index 0000000..79577a3 --- /dev/null +++ b/addons/godot-firebase/database/reference.gd.uid @@ -0,0 +1 @@ +uid://cnqrpl3aj03nh diff --git a/addons/godot-firebase/database/resource.gd b/addons/godot-firebase/database/resource.gd index c273206..ff8a620 100644 --- a/addons/godot-firebase/database/resource.gd +++ b/addons/godot-firebase/database/resource.gd @@ -1,16 +1,16 @@ ## @meta-authors SIsilicon, fenix-hub ## @meta-version 2.2 ## A generic resource used by Firebase Database. -tool +@tool class_name FirebaseResource extends Resource var key : String var data -func _init(key : String, data) -> void: - self.key = key.lstrip("/") - self.data = data +func _init(key : String,data): + self.key = key.lstrip("/") + self.data = data func _to_string(): - return "{ key:{key}, data:{data} }".format({key = key, data = data}) + return "{ key:{key}, data:{data} }".format({key = key, data = data}) diff --git a/addons/godot-firebase/database/resource.gd.uid b/addons/godot-firebase/database/resource.gd.uid new file mode 100644 index 0000000..7199ae1 --- /dev/null +++ b/addons/godot-firebase/database/resource.gd.uid @@ -0,0 +1 @@ +uid://cxrcj03uack7t diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index fe6e4a3..8f7a6c7 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -3,7 +3,7 @@ ## @meta-version 1.1 ## The dynamic links API for Firebase ## Documentation TODO. -tool +@tool class_name FirebaseDynamicLinks extends Node @@ -22,86 +22,88 @@ var _config : Dictionary = {} var _auth : Dictionary var _request_list_node : HTTPRequest -var _headers : PoolStringArray = [] +var _headers : PackedStringArray = [] enum Requests { - NONE = -1, - GENERATE + NONE = -1, + GENERATE } func _set_config(config_json : Dictionary) -> void: - _config = config_json - _request_list_node = HTTPRequest.new() - _request_list_node.connect("request_completed", self, "_on_request_completed") - add_child(_request_list_node) - _check_emulating() + _config = config_json + _request_list_node = HTTPRequest.new() + Utilities.fix_http_request(_request_list_node) + _request_list_node.request_completed.connect(_on_request_completed) + add_child(_request_list_node) + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=%s" - _base_url %= _config.apiKey - else: - var port : String = _config.emulators.ports.dynamicLinks - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Dynamic Links has not been configured.") - else: - _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=%s" + _base_url %= _config.apiKey + else: + var port : String = _config.emulators.ports.dynamicLinks + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Dynamic Links has not been configured.") + else: + _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) var _link_request_body : Dictionary = { - "dynamicLinkInfo": { - "domainUriPrefix": "", - "link": "", - "androidInfo": { - "androidPackageName": "" - }, - "iosInfo": { - "iosBundleId": "" - } - }, - "suffix": { - "option": "" - } - } + "dynamicLinkInfo": { + "domainUriPrefix": "", + "link": "", + "androidInfo": { + "androidPackageName": "" + }, + "iosInfo": { + "iosBundleId": "" + } + }, + "suffix": { + "option": "" + } +} ## @args log_link, APN, IBI, is_unguessable ## This function is used to generate a dynamic link using the Firebase REST API ## It will return a JSON with the shortened link func generate_dynamic_link(long_link : String, APN : String, IBI : String, is_unguessable : bool) -> void: - if not _config.domainUriPrefix or _config.domainUriPrefix == "": - emit_signal("generate_dynamic_link_error", "You're missing the domainUriPrefix in config file! Error!") - Firebase._printerr("You're missing the domainUriPrefix in config file! Error!") - return - - request = Requests.GENERATE - _link_request_body.dynamicLinkInfo.domainUriPrefix = _config.domainUriPrefix - _link_request_body.dynamicLinkInfo.link = long_link - _link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN - _link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI - if is_unguessable: - _link_request_body.suffix.option = "UNGUESSABLE" - else: - _link_request_body.suffix.option = "SHORT" - _request_list_node.request(_base_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_link_request_body)) - -func _on_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - var result_body = JSON.parse(body.get_string_from_utf8()) - if result_body.error: - emit_signal("generate_dynamic_link_error", result_body.error_string) - return - else: - result_body = result_body.result - - emit_signal("dynamic_link_generated", result_body.shortLink) - request = Requests.NONE + if not _config.domainUriPrefix or _config.domainUriPrefix == "": + generate_dynamic_link_error.emit("Error: Missing domainUriPrefix in config file. Parameter is required.") + Firebase._printerr("Error: Missing domainUriPrefix in config file. Parameter is required.") + return + + request = Requests.GENERATE + _link_request_body.dynamicLinkInfo.domainUriPrefix = _config.domainUriPrefix + _link_request_body.dynamicLinkInfo.link = long_link + _link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN + _link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI + if is_unguessable: + _link_request_body.suffix.option = "UNGUESSABLE" + else: + _link_request_body.suffix.option = "SHORT" + _request_list_node.request(_base_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_link_request_body)) + +func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + var json = JSON.new() + var json_parse_result = json.parse(body.get_string_from_utf8()) + if json_parse_result == OK: + var result_body = json.data.result # Check this + dynamic_link_generated.emit(result_body.shortLink) + else: + generate_dynamic_link_error.emit(json.get_error_message()) + # This used to return immediately when above, but it should still clear the request, so removing it + + request = Requests.NONE func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_logout() -> void: - _auth = {} + _auth = {} diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd.uid b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd.uid new file mode 100644 index 0000000..02f84e5 --- /dev/null +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd.uid @@ -0,0 +1 @@ +uid://c8n5k4n8yxecp diff --git a/addons/godot-firebase/example.env b/addons/godot-firebase/example.env new file mode 100644 index 0000000..e35f66a --- /dev/null +++ b/addons/godot-firebase/example.env @@ -0,0 +1,24 @@ +[firebase/environment_variables] + +"apiKey"="", +"authDomain"="", +"databaseURL"="", +"projectId"="", +"storageBucket"="", +"messagingSenderId"="", +"appId"="", +"measurementId"="" +"clientId"="" +"clientSecret"="" +"domainUriPrefix"="" +"functionsGeoZone"="" +"cacheLocation"="" + +[firebase/emulators/ports] + +authentication="" +firestore="" +realtimeDatabase="" +functions="" +storage="" +dynamicLinks="" diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index d53d0b7..f2eed9f 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -1,14 +1,15 @@ ## @meta-authors Kyle Szklenski -## @meta-version 2.5 +## @meta-version 2.6 ## The Firebase Godot API. ## This singleton gives you access to your Firebase project and its capabilities. Using this requires you to fill out some Firebase configuration settings. It currently comes with four modules. ## - [code]Auth[/code]: Manages user authentication (logging and out, etc...) ## - [code]Database[/code]: A NonSQL realtime database for managing data in JSON structures. ## - [code]Firestore[/code]: Similar to Database, but stores data in collections and documents, among other things. ## - [code]Storage[/code]: Gives access to Cloud Storage; perfect for storing files like images and other assets. +## - [code]RemoteConfig[/code]: Gives access to Remote Config functionality; allows you to download your app's configuration from Firebase, do A/B testing, and more. ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki -tool +@tool extends Node const _ENVIRONMENT_VARIABLES : String = "firebase/environment_variables" @@ -17,125 +18,127 @@ const _AUTH_PROVIDERS : String = "firebase/auth_providers" ## @type FirebaseAuth ## The Firebase Authentication API. -onready var Auth : FirebaseAuth = $Auth +@onready var Auth := $Auth ## @type FirebaseFirestore ## The Firebase Firestore API. -onready var Firestore : FirebaseFirestore = $Firestore +@onready var Firestore := $Firestore ## @type FirebaseDatabase ## The Firebase Realtime Database API. -onready var Database : FirebaseDatabase = $Database +@onready var Database := $Database ## @type FirebaseStorage ## The Firebase Storage API. -onready var Storage : FirebaseStorage = $Storage +@onready var Storage := $Storage ## @type FirebaseDynamicLinks ## The Firebase Dynamic Links API. -onready var DynamicLinks : FirebaseDynamicLinks = $DynamicLinks +@onready var DynamicLinks := $DynamicLinks ## @type FirebaseFunctions ## The Firebase Cloud Functions API -onready var Functions : FirebaseFunctions = $Functions +@onready var Functions := $Functions -export var emulating : bool = false +## @type FirebaseRemoteConfig +## The Firebase Remote Config API +@onready var RemoteConfigAPI := $RemoteConfig + +@export var emulating : bool = false # Configuration used by all files in this project # These values can be found in your Firebase Project -# See the README on Github for how to access +# See the README checked Github for how to access var _config : Dictionary = { - "apiKey": "", - "authDomain": "", - "databaseURL": "", - "projectId": "", - "storageBucket": "", - "messagingSenderId": "", - "appId": "", - "measurementId": "", - "clientId": "", - "clientSecret" : "", - "domainUriPrefix" : "", - "functionsGeoZone" : "", - "cacheLocation":"user://.firebase_cache", - "emulators": { - "ports" : { - "authentication" : "", - "firestore" : "", - "realtimeDatabase" : "", - "functions" : "", - "storage" : "", - "dynamicLinks" : "" - } - }, - "workarounds":{ - "database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329 - }, - "auth_providers": { - "facebook_id":"", - "facebook_secret":"", - "github_id":"", - "github_secret":"", - "twitter_id":"", - "twitter_secret":"" - } + "apiKey": "", + "authDomain": "", + "databaseURL": "", + "projectId": "", + "storageBucket": "", + "messagingSenderId": "", + "appId": "", + "measurementId": "", + "clientId": "", + "clientSecret" : "", + "domainUriPrefix" : "", + "functionsGeoZone" : "", + "cacheLocation":"", + "emulators": { + "ports" : { + "authentication" : "", + "firestore" : "", + "realtimeDatabase" : "", + "functions" : "", + "storage" : "", + "dynamicLinks" : "" + } + }, + "workarounds":{ + "database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329 + }, + "auth_providers": { + "facebook_id":"", + "facebook_secret":"", + "github_id":"", + "github_secret":"", + "twitter_id":"", + "twitter_secret":"" + } } func _ready() -> void: - _load_config() + _load_config() func set_emulated(emulating : bool = true) -> void: - self.emulating = emulating - _check_emulating() + self.emulating = emulating + _check_emulating() func _check_emulating() -> void: - if emulating: - print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.") - for module in get_children(): - if module.has_method("_check_emulating"): - module._check_emulating() + if emulating: + print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.") + for module in get_children(): + if module.has_method("_check_emulating"): + module._check_emulating() func _load_config() -> void: - if _config.apiKey != "" and _config.authDomain != "": - pass - else: - var env = ConfigFile.new() - var err = env.load("res://addons/godot-firebase/.env") - if err == OK: - for key in _config.keys(): - if key == "emulators": - for port in _config[key]["ports"].keys(): - _config[key]["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") - if key == "auth_providers": - for provider in _config[key].keys(): - _config[key][provider] = env.get_value(_AUTH_PROVIDERS, provider) - else: - var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") - if value == "": - _print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key) - else: - _config[key] = value - else: - _printerr("Unable to read .env file at path 'res://addons/godot-firebase/.env'") - - _setup_modules() + if not (_config.apiKey != "" and _config.authDomain != ""): + var env = ConfigFile.new() + var err = env.load("res://addons/godot-firebase/.env") + if err == OK: + for key in _config.keys(): + var config_value = _config[key] + if key == "emulators" and config_value.has("ports"): + for port in config_value["ports"].keys(): + config_value["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") + if key == "auth_providers": + for provider in config_value.keys(): + config_value[provider] = env.get_value(_AUTH_PROVIDERS, provider, "") + else: + var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") + if value == "": + _print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key) + else: + _config[key] = value + else: + _printerr("Unable to read .env file at path 'res://addons/godot-firebase/.env'") + + _setup_modules() func _setup_modules() -> void: - for module in get_children(): - module._set_config(_config) - if not module.has_method("_on_FirebaseAuth_login_succeeded"): - continue - Auth.connect("login_succeeded", module, "_on_FirebaseAuth_login_succeeded") - Auth.connect("signup_succeeded", module, "_on_FirebaseAuth_login_succeeded") - Auth.connect("token_refresh_succeeded", module, "_on_FirebaseAuth_token_refresh_succeeded") - Auth.connect("logged_out", module, "_on_FirebaseAuth_logout") - + for module in get_children(): + module._set_config(_config) + if not module.has_method("_on_FirebaseAuth_login_succeeded"): + continue + Auth.login_succeeded.connect(module._on_FirebaseAuth_login_succeeded) + Auth.signup_succeeded.connect(module._on_FirebaseAuth_login_succeeded) + Auth.token_refresh_succeeded.connect(module._on_FirebaseAuth_token_refresh_succeeded) + Auth.logged_out.connect(module._on_FirebaseAuth_logout) # ------------- func _printerr(error : String) -> void: - printerr("[Firebase Error] >> "+error) + printerr("[Firebase Error] >> " + error) func _print(msg : String) -> void: - print("[Firebase] >> "+msg) + print("[Firebase] >> " + str(msg)) diff --git a/addons/godot-firebase/firebase/firebase.gd.uid b/addons/godot-firebase/firebase/firebase.gd.uid new file mode 100644 index 0000000..0c134e0 --- /dev/null +++ b/addons/godot-firebase/firebase/firebase.gd.uid @@ -0,0 +1 @@ +uid://daly44phfdkqa diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index 6b86131..31f5b56 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -1,31 +1,36 @@ -[gd_scene load_steps=8 format=2] +[gd_scene load_steps=9 format=3 uid="uid://cvb26atjckwlq"] -[ext_resource path="res://addons/godot-firebase/database/database.gd" type="Script" id=1] -[ext_resource path="res://addons/godot-firebase/firestore/firestore.gd" type="Script" id=2] -[ext_resource path="res://addons/godot-firebase/firebase/firebase.gd" type="Script" id=3] -[ext_resource path="res://addons/godot-firebase/auth/auth.gd" type="Script" id=4] -[ext_resource path="res://addons/godot-firebase/storage/storage.gd" type="Script" id=5] -[ext_resource path="res://addons/godot-firebase/dynamiclinks/dynamiclinks.gd" type="Script" id=6] -[ext_resource path="res://addons/godot-firebase/functions/functions.gd" type="Script" id=7] +[ext_resource type="Script" path="res://addons/godot-firebase/database/database.gd" id="1"] +[ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-firebase/firebase/firebase.gd" id="3"] +[ext_resource type="Script" path="res://addons/godot-firebase/auth/auth.gd" id="4"] +[ext_resource type="Script" path="res://addons/godot-firebase/storage/storage.gd" id="5"] +[ext_resource type="Script" path="res://addons/godot-firebase/dynamiclinks/dynamiclinks.gd" id="6"] +[ext_resource type="Script" path="res://addons/godot-firebase/functions/functions.gd" id="7"] +[ext_resource type="PackedScene" uid="uid://5xa6ulbllkjk" path="res://addons/godot-firebase/remote_config/firebase_remote_config.tscn" id="8_mvdf4"] [node name="Firebase" type="Node"] -pause_mode = 2 -script = ExtResource( 3 ) +script = ExtResource("3") [node name="Auth" type="HTTPRequest" parent="."] -script = ExtResource( 4 ) +max_redirects = 12 +timeout = 10.0 +script = ExtResource("4") [node name="Firestore" type="Node" parent="."] -script = ExtResource( 2 ) +script = ExtResource("2") [node name="Database" type="Node" parent="."] -script = ExtResource( 1 ) +script = ExtResource("1") [node name="Storage" type="Node" parent="."] -script = ExtResource( 5 ) +script = ExtResource("5") [node name="DynamicLinks" type="Node" parent="."] -script = ExtResource( 6 ) +script = ExtResource("6") [node name="Functions" type="Node" parent="."] -script = ExtResource( 7 ) +script = ExtResource("7") + +[node name="RemoteConfig" parent="." instance=ExtResource("8_mvdf4")] +accept_gzip = false diff --git a/addons/godot-firebase/firestore/field_transform.gd b/addons/godot-firebase/firestore/field_transform.gd new file mode 100644 index 0000000..b69395b --- /dev/null +++ b/addons/godot-firebase/firestore/field_transform.gd @@ -0,0 +1,22 @@ +extends FirestoreTransform +class_name FieldTransform + +enum TransformType { SetToServerValue, Maximum, Minimum, Increment, AppendMissingElements, RemoveAllFromArray } + +const transtype_string_map = { + TransformType.SetToServerValue : "setToServerValue", + TransformType.Increment : "increment", + TransformType.Maximum : "maximum", + TransformType.Minimum : "minimum", + TransformType.AppendMissingElements : "appendMissingElements", + TransformType.RemoveAllFromArray : "removeAllFromArray" +} + +var document_exists : bool +var document_name : String +var field_path : String +var transform_type : TransformType +var value : Variant + +func get_transform_type() -> String: + return transtype_string_map[transform_type] diff --git a/addons/godot-firebase/firestore/field_transform.gd.uid b/addons/godot-firebase/firestore/field_transform.gd.uid new file mode 100644 index 0000000..afa06f9 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transform.gd.uid @@ -0,0 +1 @@ +uid://m8u3aaxvj12y diff --git a/addons/godot-firebase/firestore/field_transform_array.gd b/addons/godot-firebase/firestore/field_transform_array.gd new file mode 100644 index 0000000..72552e9 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transform_array.gd @@ -0,0 +1,35 @@ +class_name FieldTransformArray +extends RefCounted + +var transforms = [] + +var _extended_url +var _collection_name +const _separator = "/" + +func set_config(config : Dictionary): + _extended_url = config.extended_url + _collection_name = config.collection_name + +func push_back(transform : FieldTransform) -> void: + transforms.push_back(transform) + +func serialize() -> Dictionary: + var body = {} + var writes_array = [] + for transform in transforms: + writes_array.push_back({ + "currentDocument": { "exists" : transform.document_exists }, + "transform" : { + "document": _extended_url + _collection_name + _separator + transform.document_name, + "fieldTransforms": [ + { + "fieldPath": transform.field_path, + transform.get_transform_type(): transform.value + }] + } + }) + + body = { "writes": writes_array } + + return body diff --git a/addons/godot-firebase/firestore/field_transform_array.gd.uid b/addons/godot-firebase/firestore/field_transform_array.gd.uid new file mode 100644 index 0000000..c8c1e1a --- /dev/null +++ b/addons/godot-firebase/firestore/field_transform_array.gd.uid @@ -0,0 +1 @@ +uid://d0jvpbt4ahgfn diff --git a/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd b/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd new file mode 100644 index 0000000..ed7f4b7 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd @@ -0,0 +1,19 @@ +class_name DecrementTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.Increment + + var value_type = typeof(by_this_much) + if value_type == TYPE_INT: + self.value = { + "integerValue": -by_this_much + } + elif value_type == TYPE_FLOAT: + self.value = { + "doubleValue": -by_this_much + } diff --git a/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd.uid b/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd.uid new file mode 100644 index 0000000..2e6d2d1 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/decrement_transform.gd.uid @@ -0,0 +1 @@ +uid://bm3s8h247w8ne diff --git a/addons/godot-firebase/firestore/field_transforms/increment_transform.gd b/addons/godot-firebase/firestore/field_transforms/increment_transform.gd new file mode 100644 index 0000000..5c7a38c --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/increment_transform.gd @@ -0,0 +1,19 @@ +class_name IncrementTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.Increment + + var value_type = typeof(by_this_much) + if value_type == TYPE_INT: + self.value = { + "integerValue": by_this_much + } + elif value_type == TYPE_FLOAT: + self.value = { + "doubleValue": by_this_much + } diff --git a/addons/godot-firebase/firestore/field_transforms/increment_transform.gd.uid b/addons/godot-firebase/firestore/field_transforms/increment_transform.gd.uid new file mode 100644 index 0000000..a14f5b6 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/increment_transform.gd.uid @@ -0,0 +1 @@ +uid://b1w5ax2auogrv diff --git a/addons/godot-firebase/firestore/field_transforms/max_transform.gd b/addons/godot-firebase/firestore/field_transforms/max_transform.gd new file mode 100644 index 0000000..a10c87e --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/max_transform.gd @@ -0,0 +1,19 @@ +class_name MaxTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.Maximum + + var value_type = typeof(value) + if value_type == TYPE_INT: + self.value = { + "integerValue": value + } + elif value_type == TYPE_FLOAT: + self.value = { + "doubleValue": value + } diff --git a/addons/godot-firebase/firestore/field_transforms/max_transform.gd.uid b/addons/godot-firebase/firestore/field_transforms/max_transform.gd.uid new file mode 100644 index 0000000..05e804f --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/max_transform.gd.uid @@ -0,0 +1 @@ +uid://cy6ck07xmev8s diff --git a/addons/godot-firebase/firestore/field_transforms/min_transform.gd b/addons/godot-firebase/firestore/field_transforms/min_transform.gd new file mode 100644 index 0000000..82fd8e4 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/min_transform.gd @@ -0,0 +1,19 @@ +class_name MinTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.Minimum + + var value_type = typeof(value) + if value_type == TYPE_INT: + self.value = { + "integerValue": value + } + elif value_type == TYPE_FLOAT: + self.value = { + "doubleValue": value + } diff --git a/addons/godot-firebase/firestore/field_transforms/min_transform.gd.uid b/addons/godot-firebase/firestore/field_transforms/min_transform.gd.uid new file mode 100644 index 0000000..fb8fcf6 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/min_transform.gd.uid @@ -0,0 +1 @@ +uid://6iu7e3yb6jl6 diff --git a/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd b/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd new file mode 100644 index 0000000..7c7c380 --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd @@ -0,0 +1,10 @@ +class_name ServerTimestampTransform +extends FieldTransform + +func _init(doc_name : String, doc_must_exist : bool, path_to_field : String) -> void: + document_name = doc_name + document_exists = doc_must_exist + field_path = path_to_field + + transform_type = FieldTransform.TransformType.SetToServerValue + value = "REQUEST_TIME" diff --git a/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd.uid b/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd.uid new file mode 100644 index 0000000..41b3ead --- /dev/null +++ b/addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd.uid @@ -0,0 +1 @@ +uid://coog5qkoasix diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 156a645..be97605 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -12,27 +12,19 @@ ## (source: [url=https://firebase.google.com/docs/firestore]Firestore[/url]) ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore -tool +@tool class_name FirebaseFirestore extends Node const _API_VERSION : String = "v1" -## Emitted when a [code]list()[/code] request is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types Array -signal listed_documents(documents) -## Emitted when a [code]query()[/code] request is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types Array -signal result_query(result) -## Emitted when a [code]query()[/code] request is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types Array ## Emitted when a [code]list()[/code] or [code]query()[/code] request is [b]not[/b] successfully completed. -signal task_error(code,status,message) +signal error(code, status, message) enum Requests { - NONE = -1, ## Firestore is not processing any request. - LIST, ## Firestore is processing a [code]list()[/code] request on a collection. - QUERY ## Firestore is processing a [code]query()[/code] request on a collection. + NONE = -1, ## Firestore is not processing any request. + LIST, ## Firestore is processing a [code]list()[/code] request checked a collection. + QUERY ## Firestore is processing a [code]query()[/code] request checked a collection. } # TODO: Implement cache size limit @@ -48,333 +40,205 @@ const _MAX_POOLED_REQUEST_AGE = 30 ## The code indicating the request Firestore is processing. ## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers. ## @enum Requests -var request : int = -1 - -## Whether cache files can be used and generated. -## @default true -var persistence_enabled : bool = true - -## Whether an internet connection can be used. -## @default true -var networking: bool = true setget set_networking - -## A Dictionary containing all collections currently referenced. -## @type Dictionary -var collections : Dictionary = {} +var request: int = -1 ## A Dictionary containing all authentication fields for the current logged user. ## @type Dictionary -var auth : Dictionary +var auth: Dictionary -var _config : Dictionary = {} +var _config: Dictionary = {} var _cache_loc: String -var _encrypt_key := "5vg76n90345f7w390346" if OS.get_name() in ["HTML5", "UWP"] else OS.get_unique_id() +var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_unique_id() -var _base_url : String = "" -var _extended_url : String = "projects/[PROJECT_ID]/databases/(default)/documents/" -var _query_suffix : String = ":runQuery" +var _base_url: String = "" +var _extended_url: String = "projects/{PROJECT_ID}/databases/{DATABASE_NAME}/documents/" +var _query_suffix: String = ":runQuery" +var _agg_query_suffix: String = ":runAggregationQuery" #var _connect_check_node : HTTPRequest -var _request_list_node : HTTPRequest -var _requests_queue : Array = [] -var _current_query : FirestoreQuery - -var _http_request_pool := [] - -var _offline: bool = false setget _set_offline - -func _ready() -> void: - pass - -func _process(delta : float) -> void: - for i in range(_http_request_pool.size() - 1, -1, -1): - var request = _http_request_pool[i] - if not request.get_meta("requesting"): - var lifetime: float = request.get_meta("lifetime") + delta - if lifetime > _MAX_POOLED_REQUEST_AGE: - request.queue_free() - _http_request_pool.remove(i) - request.set_meta("lifetime", lifetime) - +var _request_list_node: HTTPRequest +var _requests_queue: Array = [] +var _current_query: FirestoreQuery ## Returns a reference collection by its [i]path[/i]. ## ## The returned object will be of [code]FirestoreCollection[/code] type. -## If saved into a variable, it can be used to issue requests on the collection itself. +## If saved into a variable, it can be used to issue requests checked the collection itself. ## @args path ## @return FirestoreCollection func collection(path : String) -> FirestoreCollection: - if not collections.has(path): - var coll : FirestoreCollection = FirestoreCollection.new() - coll._extended_url = _extended_url - coll._base_url = _base_url - coll._config = _config - coll.auth = auth - coll.collection_name = path - coll.firestore = self - collections[path] = coll - return coll - else: - return collections[path] - - -## Issue a query on your Firestore database. + for coll in get_children(): + if coll is FirestoreCollection: + if coll.collection_name == path: + return coll + + var coll : FirestoreCollection = FirestoreCollection.new() + coll._extended_url = _extended_url + coll._base_url = _base_url + coll._config = _config + coll.auth = auth + coll.collection_name = path + add_child(coll) + return coll + + +## Issue a query checked your Firestore database. ## ## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query. -## This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. -## If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield on the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. +## When awaited, this function returns the resulting array from the query. ## ## ex. -## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code] -## [code]yield(query_task, "task_finished")[/code] -## Since the emitted signal is holding an argument, it can be directly retrieved as a return variable from the [code]yield()[/code] function. -## -## ex. -## [code]var result : Array = yield(query_task, "task_finished")[/code] +## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code] ## ## [b]Warning:[/b] It currently does not work offline! ## ## @args query ## @arg-types FirestoreQuery -## @return FirestoreTask -func query(query : FirestoreQuery) -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.connect("result_query", self, "_on_result_query") - firestore_task.connect("task_error", self, "_on_task_error") - firestore_task.action = FirestoreTask.Task.TASK_QUERY - var body : Dictionary = { structuredQuery = query.query } - var url : String = _base_url + _extended_url + _query_suffix - - firestore_task.data = query - firestore_task._fields = JSON.print(body) - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task - - -## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield on the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. -## [b]Note:[/b] [code]order_by[/code] does not work in offline mode. -## ex. -## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code] -## [code]yield(query_task, "task_finished")[/code] -## Since the emitted signal is holding an argument, it can be directly retrieved as a return variable from the [code]yield()[/code] function. +## @return Array[FirestoreDocument] +func query(query : FirestoreQuery) -> Array: + if query.aggregations.size() > 0: + Firebase._printerr("Aggregation query sent with normal query call: " + str(query)) + return [] + + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_QUERY + var body: Dictionary = { structuredQuery = query.query } + var url: String = _base_url + _extended_url + query.sub_collection_path + _query_suffix + + task.data = query + task._fields = JSON.stringify(body) + task._url = url + _pooled_request(task) + return await _handle_task_finished(task) + +## Issue an aggregation query (sum, average, count) against your Firestore database; +## cheaper than a normal query and counting (for instance) values directly. +## +## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query. +## When awaited, this function returns the result from the aggregation query. ## ## ex. -## [code]var result : Array = yield(query_task, "task_finished")[/code] +## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code] ## +## [b]Warning:[/b] It currently does not work offline! +## +## @args query +## @arg-types FirestoreQuery +## @return Variant representing the array results of the aggregation query +func aggregation_query(query : FirestoreQuery) -> Variant: + if query.aggregations.size() == 0: + Firebase._printerr("Aggregation query sent with no aggregation values: " + str(query)) + return 0 + + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_AGG_QUERY + + var body: Dictionary = { structuredAggregationQuery = { structuredQuery = query.query, aggregations = query.aggregations } } + var url: String = _base_url + _extended_url + _agg_query_suffix + + task.data = query + task._fields = JSON.stringify(body) + task._url = url + _pooled_request(task) + var result = await _handle_task_finished(task) + return result + +## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return an Array[FirestoreDocument] ## @args collection_id, page_size, page_token, order_by ## @arg-types String, int, String, String ## @arg-defaults , 0, "", "" -## @return FirestoreTask -func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.connect("listed_documents", self, "_on_listed_documents") - firestore_task.connect("task_error", self, "_on_task_error") - firestore_task.action = FirestoreTask.Task.TASK_LIST - var url : String = _base_url + _extended_url + path - if page_size != 0: - url+="?pageSize="+str(page_size) - if page_token != "": - url+="&pageToken="+page_token - if order_by != "": - url+="&orderBy="+order_by - - firestore_task.data = [path, page_size, page_token, order_by] - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task - - -func set_networking(value: bool) -> void: - if value: - enable_networking() - else: - disable_networking() - - -func enable_networking() -> void: - if networking: - return - networking = true - _base_url = _base_url.replace("storeoffline", "firestore") - for key in collections: - collections[key]._base_url = _base_url - - -func disable_networking() -> void: - if not networking: - return - networking = false - # Pointing to an invalid url should do the trick. - _base_url = _base_url.replace("firestore", "storeoffline") - for key in collections: - collections[key]._base_url = _base_url - - -func _set_offline(value: bool) -> void: - if value == _offline: - return - - _offline = value - if not persistence_enabled: - return - - var event_record_path: String = _config["cacheLocation"].plus_file(_CACHE_RECORD_FILE) - if not value: - var offline_time := 2147483647 # Maximum signed 32-bit integer - var file := File.new() - if file.open_encrypted_with_pass(event_record_path, File.READ, _encrypt_key) == OK: - offline_time = int(file.get_buffer(file.get_len()).get_string_from_utf8()) - 2 - file.close() - - var cache_dir := Directory.new() - var cache_files := [] - if cache_dir.open(_cache_loc) == OK: - cache_dir.list_dir_begin(true) - var file_name = cache_dir.get_next() - while file_name != "": - if not cache_dir.current_is_dir() and file_name.ends_with(_CACHE_EXTENSION): - if file.get_modified_time(_cache_loc.plus_file(file_name)) >= offline_time: - cache_files.append(_cache_loc.plus_file(file_name)) -# else: -# print("%s is old! It's time is %d, but the time offline was %d." % [file_name, file.get_modified_time(_cache_loc.plus_file(file_name)), offline_time]) - file_name = cache_dir.get_next() - cache_dir.list_dir_end() - - cache_files.erase(event_record_path) - cache_dir.remove(event_record_path) - - for cache in cache_files: - var deleted := false - if file.open_encrypted_with_pass(cache, File.READ, _encrypt_key) == OK: - var name := file.get_line() - var content := file.get_line() - var collection_id := name.left(name.find_last("/")) - var document_id := name.right(name.find_last("/") + 1) - - var collection := collection(collection_id) - if content == "--deleted--": - collection.delete(document_id) - deleted = true - else: - collection.update(document_id, FirestoreDocument.fields2dict(JSON.parse(content).result)) - else: - Firebase._printerr("Failed to retrieve cache %s! Error code: %d" % [cache, file.get_error()]) - file.close() - if deleted: - cache_dir.remove(cache) - - else: - var file := File.new() - if file.open_encrypted_with_pass(event_record_path, File.WRITE, _encrypt_key) == OK: - file.store_buffer(str(OS.get_unix_time()).to_utf8()) - file.close() +## @return Array[FirestoreDocument] +func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> Array: + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_LIST + var url : String = _base_url + _extended_url + path + if page_size != 0: + url+="?pageSize="+str(page_size) + if page_token != "": + url+="&pageToken="+page_token + if order_by != "": + url+="&orderBy="+order_by + + task.data = [path, page_size, page_token, order_by] + task._url = url + _pooled_request(task) + + return await _handle_task_finished(task) func _set_config(config_json : Dictionary) -> void: - _config = config_json - _cache_loc = _config["cacheLocation"] - _extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId) - - var file := File.new() - if file.file_exists(_cache_loc.plus_file(_CACHE_RECORD_FILE)): - _offline = true - else: - _offline = false - - _check_emulating() + _config = config_json + _cache_loc = _config["cacheLocation"] + if _config.databaseName.is_empty(): + _config.databaseName = "(default)" + _extended_url = _extended_url.format({"PROJECT_ID":_config.projectId, "DATABASE_NAME":_config.databaseName}) + # Since caching is causing a lot of issues, I'm removing this check for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://firestore.googleapis.com/{version}/".format({ version = _API_VERSION }) - else: - var port : String = _config.emulators.ports.firestore - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Firestore has not been configured.") - else: - _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://firestore.googleapis.com/{version}/".format({ version = _API_VERSION }) + else: + var port : String = _config.emulators.ports.firestore + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Firestore has not been configured.") + else: + _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) func _pooled_request(task : FirestoreTask) -> void: - if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) - return - - if not auth and not Firebase.emulating: - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") - if result[0] != 1: - _check_auth_error(result[0], result[1]) - Firebase._print("Client connected as Anonymous") - - if not Firebase.emulating: - task._headers = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - - var http_request : HTTPRequest - for request in _http_request_pool: - if not request.get_meta("requesting"): - http_request = request - break - - if not http_request: - http_request = HTTPRequest.new() - http_request.timeout = 5 - _http_request_pool.append(http_request) - add_child(http_request) - http_request.connect("request_completed", self, "_on_pooled_request_completed", [http_request]) - - http_request.set_meta("requesting", true) - http_request.set_meta("lifetime", 0.0) - http_request.set_meta("task", task) - http_request.request(task._url, task._headers, !Firebase.emulating, task._method, task._fields) - - -# ------------- - - -func _on_listed_documents(listed_documents : Array): - emit_signal("listed_documents", listed_documents) - - -func _on_result_query(result : Array): - emit_signal("result_query", result) - -func _on_task_error(code : int, status : String, message : String, task : int): - emit_signal("task_error", code, status, message) - Firebase._printerr(message) + if (auth == null or auth.is_empty()) and not Firebase.emulating: + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = await Firebase.Auth.auth_request + if result[0] != 1: + _check_auth_error(result[0], result[1]) + Firebase._print("Client connected as Anonymous") + + if not Firebase.emulating: + task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + + var http_request = HTTPRequest.new() + http_request.timeout = 5 + Utilities.fix_http_request(http_request) + add_child(http_request) + http_request.request_completed.connect( + func(result, response_code, headers, body): + task._on_request_completed(result, response_code, headers, body) + http_request.queue_free() + ) + + http_request.request(task._url, task._headers, task._method, task._fields) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - auth = auth_result - for key in collections: - collections[key].auth = auth - + auth = auth_result + for coll in get_children(): + if coll is FirestoreCollection: + coll.auth = auth func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - auth = auth_result - for key in collections: - collections[key].auth = auth - - -func _on_pooled_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest) -> void: - request.get_meta("task")._on_request_completed(result, response_code, headers, body) - request.set_meta("requesting", false) - - -func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: - _set_offline(result != HTTPRequest.RESULT_SUCCESS) - #_connect_check_node.request(_base_url) - + auth = auth_result + for coll in get_children(): + if coll is FirestoreCollection: + coll.auth = auth func _on_FirebaseAuth_logout() -> void: - auth = {} + auth = {} func _check_auth_error(code : int, message : String) -> void: - var err : String - match code: - 400: err = "Please enable the Anonymous Sign-in method, or Authenticate the Client before issuing a request" - Firebase._printerr(err) - Firebase._printerr(message) + var err : String + match code: + 400: err = "Please enable the Anonymous Sign-in method, or Authenticate the Client before issuing a request" + Firebase._printerr(err) + Firebase._printerr(message) + +func _handle_task_finished(task : FirestoreTask): + await task.task_finished + + if task.error.keys().size() > 0: + error.emit(task.error) + + return task.data diff --git a/addons/godot-firebase/firestore/firestore.gd.uid b/addons/godot-firebase/firestore/firestore.gd.uid new file mode 100644 index 0000000..6b9d05f --- /dev/null +++ b/addons/godot-firebase/firestore/firestore.gd.uid @@ -0,0 +1 @@ +uid://coopw3mtkrnqk diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 7903318..7c73f69 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -3,15 +3,11 @@ ## @meta-version 2.3 ## A reference to a Firestore Collection. ## Documentation TODO. -tool +@tool class_name FirestoreCollection -extends Reference +extends Node -signal add_document(doc) -signal get_document(doc) -signal update_document(doc) -signal delete_document() -signal error(code,status,message) +signal error(error_result) const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " @@ -21,124 +17,205 @@ const _documentId_tag : String = "documentId=" var auth : Dictionary var collection_name : String -var firestore # FirebaseFirestore (can't static type due to cyclic reference) var _base_url : String var _extended_url : String var _config : Dictionary var _documents := {} -var _request_queues := {} # ----------------------- Requests ## @args document_id ## @return FirestoreTask ## used to GET a document from the collection, specify @document_id -func get(document_id : String) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_GET - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - - task.connect("get_document", self, "_on_get_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task +func get_doc(document_id : String, from_cache : bool = false, is_listener : bool = false) -> FirestoreDocument: + if from_cache: + # for now, just return the child directly; in the future, make it smarter so there's a default, if long, polling time for this + for child in get_children(): + if child.doc_name == document_id: + return child + + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_GET + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + + _process_request(task, document_id, url) + var result = await Firebase.Firestore._handle_task_finished(task) + if result != null: + var found = false + for child in get_children(): + if child.doc_name == document_id: + child.replace(result, true) + result = child + found = true + break + + if not found: + add_child(result, true) + + result.collection_name = collection_name + else: + print("get_document returned null for %s %s" % [collection_name, document_id]) + + return result ## @args document_id, fields ## @arg-defaults , {} -## @return FirestoreTask -## used to SAVE/ADD a new document to the collection, specify @documentID and @fields -func add(document_id : String, fields : Dictionary = {}) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_POST - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _query_tag + _documentId_tag + document_id - - task.connect("add_document", self, "_on_add_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.print(FirestoreDocument.dict2fields(fields))) - return task - -## @args document_id, fields -## @arg-defaults , {} -## @return FirestoreTask -# used to UPDATE a document, specify @documentID and @fields -func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_PATCH - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + "?" - for key in fields.keys(): - url+="updateMask.fieldPaths={key}&".format({key = key}) - url = url.rstrip("&") - - task.connect("update_document", self, "_on_update_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.print(FirestoreDocument.dict2fields(fields))) - return task +## @return FirestoreDocument +## used to ADD a new document to the collection, specify @documentID and @data +func add(document_id : String, data : Dictionary = {}) -> FirestoreDocument: + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_POST + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _query_tag + _documentId_tag + document_id + + _process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data))) + var result = await Firebase.Firestore._handle_task_finished(task) + if result != null: + for child in get_children(): + if child.doc_name == document_id: + child.free() # Consider throwing an error for this since it shouldn't already exist + break + + result.collection_name = collection_name + add_child(result, true) + return result + +## @args document +## @return FirestoreDocument +# used to UPDATE a document, specify the document +func update(document : FirestoreDocument) -> FirestoreDocument: + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_PATCH + task.data = collection_name + "/" + document.doc_name + var url = _get_request_url() + _separator + document.doc_name.replace(" ", "%20") + "?" + for key in document.keys(): + url+="updateMask.fieldPaths={key}&".format({key = key}) + + url = url.rstrip("&") + + for key in document.keys(): + if document.get_value(key) == null: + document._erase(key) + + var temp_transforms + if document._transforms != null: + temp_transforms = document._transforms + document._transforms = null + + var body = JSON.stringify({"fields": document.document}) + + _process_request(task, document.doc_name, url, body) + var result = await Firebase.Firestore._handle_task_finished(task) + if result != null: + for child in get_children(): + if child.doc_name == result.doc_name: + child.replace(result, true) + break + result.collection_name = collection_name + + if temp_transforms != null: + result._transforms = temp_transforms + + return result + + +## @args document +## @return Dictionary +# Used to commit changes from transforms, specify the document with the transforms +func commit(document : FirestoreDocument) -> Dictionary: + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_COMMIT + var url = get_database_url("commit") + + document._transforms.set_config( + { + "extended_url": _extended_url, + "collection_name": collection_name + } + ) # Only place we can set this is here, oofness + + var body = document._transforms.serialize() + # Capture transforms to map results back to fields + var applied_transforms = document._transforms.transforms.duplicate() + document.clear_field_transforms() + + _process_request(task, document.doc_name, url, JSON.stringify(body)) + var result = await Firebase.Firestore._handle_task_finished(task) + + if result and result.has("writeResults"): + var write_results = result.writeResults + for i in range(write_results.size()): + var write_result = write_results[i] + if i < applied_transforms.size(): + var transform = applied_transforms[i] + if write_result.has("transformResults") and write_result.transformResults.size() > 0: + # Each write has one transform, so one transformResult + var new_value = Utilities.from_firebase_type(write_result.transformResults[0]) + document.add_or_update_field(transform.field_path, new_value) + + return result if result else task.error ## @args document_id ## @return FirestoreTask -# used to DELETE a document, specify @document_id -func delete(document_id : String) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_DELETE - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - - task.connect("delete_document", self, "_on_delete_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task - -# ----------------- Functions -func _get_request_url() -> String: - return _base_url + _extended_url + collection_name +# used to DELETE a document, specify the document +func delete(document : FirestoreDocument) -> bool: + var doc_name = document.doc_name + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_DELETE + task.data = document.collection_name + "/" + doc_name + var url = _get_request_url() + _separator + doc_name.replace(" ", "%20") + _process_request(task, doc_name, url) + var result = await Firebase.Firestore._handle_task_finished(task) + + # Clean up the cache + if result: + for node in get_children(): + if node.doc_name == doc_name: + node.free() # Should be only one + break + + return result +func _get_request_url() -> String: + return _base_url + _extended_url + collection_name func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: - task.connect("task_error", self, "_on_error") - - if not auth: - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") - if result[0] != 1: - Firebase.Firestore._check_auth_error(result[0], result[1]) - return null - Firebase._print("Client authenticated as Anonymous User.") - - task._url = url - task._fields = fields - task._headers = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - if _request_queues.has(document_id) and not _request_queues[document_id].empty(): - _request_queues[document_id].append(task) - else: - _request_queues[document_id] = [] - firestore._pooled_request(task) -# task._push_request(url, , fields) - - -func _on_task_finished(task : FirestoreTask, document_id : String) -> void: - if not _request_queues[document_id].empty(): - task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) - - -# -------------------- Higher level of communication with signals -func _on_get_document(document : FirestoreDocument): - emit_signal("get_document", document ) - -func _on_add_document(document : FirestoreDocument): - emit_signal("add_document", document ) - -func _on_update_document(document : FirestoreDocument): - emit_signal("update_document", document ) - -func _on_delete_document(): - emit_signal("delete_document") - -func _on_error(code, status, message, task): - emit_signal("error", code, status, message) - Firebase._printerr(message) + if auth == null or auth.is_empty(): + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = await Firebase.Auth.auth_request + if result[0] != 1: + Firebase.Firestore._check_auth_error(result[0], result[1]) + return + Firebase._print("Client authenticated as Anonymous User.") + + task._url = url + task._fields = fields + task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + Firebase.Firestore._pooled_request(task) + +func get_database_url(append) -> String: + return _base_url + _extended_url.rstrip("/") + ":" + append + +## @args document_id: StringName, data: Variant +## @return void +# used to SET a document, specify the document ID and new data +func set_doc(document_id: StringName, data: Variant) -> void: + var task: FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_PATCH + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + + _process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data))) + var result = await Firebase.Firestore._handle_task_finished(task) + + if result != null: + for child in get_children(): + if child.doc_name == document_id: + child.replace(result, true) + break + else: + print("set_document returned null for %s %s" % [collection_name, document_id]) diff --git a/addons/godot-firebase/firestore/firestore_collection.gd.uid b/addons/godot-firebase/firestore/firestore_collection.gd.uid new file mode 100644 index 0000000..d20ab95 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_collection.gd.uid @@ -0,0 +1 @@ +uid://bv82me440mg22 diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index 87e7e51..23a37ab 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -1,10 +1,10 @@ -## @meta-authors TODO +## @meta-authors Kyle Szklenski ## @meta-version 2.2 ## A reference to a Firestore Document. ## Documentation TODO. -tool +@tool class_name FirestoreDocument -extends Reference +extends Node # A FirestoreDocument objects that holds all important values for a Firestore Document, # @doc_name = name of the Firestore Document, which is the request PATH @@ -12,150 +12,180 @@ extends Reference # created when requested from a `collection().get()` call var document : Dictionary # the Document itself -var doc_fields : Dictionary # only .fields var doc_name : String # only .name var create_time : String # createTime +var collection_name : String # Name of the collection to which it belongs +var _transforms : FieldTransformArray # The transforms to apply +signal changed(changes) -func _init(doc : Dictionary = {}, _doc_name : String = "", _doc_fields : Dictionary = {}) -> void: - self.document = doc - self.doc_name = doc.name - if self.doc_name.count("/") > 2: - self.doc_name = (self.doc_name.split("/") as Array).back() - self.doc_fields = fields2dict(self.document) - self.create_time = doc.createTime +func _init(doc : Dictionary = {}): + _transforms = FieldTransformArray.new() + + if doc.has("fields"): + document = doc.fields + if doc.has("name"): + doc_name = doc.name + if doc_name.count("/") > 2: + doc_name = (doc_name.split("/") as Array).back() + if doc.has("createTime"): + self.create_time = doc.createTime -# Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields -# Field Path using the "dot" (`.`) notation are supported: -# ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } } -static func dict2fields(dict : Dictionary) -> Dictionary: - var fields : Dictionary = {} - var var_type : String = "" - for field in dict.keys(): - var field_value = dict[field] - if "." in field: - var keys: Array = field.split(".") - field = keys.pop_front() - keys.invert() - for key in keys: - field_value = { key : field_value } - match typeof(field_value): - TYPE_NIL: var_type = "nullValue" - TYPE_BOOL: var_type = "booleanValue" - TYPE_INT: var_type = "integerValue" - TYPE_REAL: var_type = "doubleValue" - TYPE_STRING: var_type = "stringValue" - TYPE_DICTIONARY: - if is_field_timestamp(field_value): - var_type = "timestampValue" - field_value = dict2timestamp(field_value) - else: - var_type = "mapValue" - field_value = dict2fields(field_value) - TYPE_ARRAY: - var_type = "arrayValue" - field_value = {"values": array2fields(field_value)} +func replace(with : FirestoreDocument, is_listener := false) -> void: + var current = document.duplicate() + document = with.document + + var changes = { + "added": [], "removed": [], "updated": [] + } + + for key in current.keys(): + if not document.has(key): + changes.removed.push_back({ "key" : key }) + else: + var new_value = Utilities.from_firebase_type(document[key]) + var old_value = Utilities.from_firebase_type(current[key]) + if typeof(new_value) != typeof(old_value) or new_value != old_value: + changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value }) + + for key in document.keys(): + if not current.has(key): + changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) }) + + if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()): + _emit_changes(changes) + +func _emit_changes(changes) -> void: + var listener_found = false + for child in get_children(): + if child is FirestoreListener: + child.send_change(changes) + listener_found = true + break + + if not listener_found: + changed.emit(changes) - if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"): - for key in field_value["fields"].keys(): - fields[field]["mapValue"]["fields"][key] = field_value["fields"][key] - else: - fields[field] = { var_type : field_value } - return {'fields' : fields} +func new_document(base_document: Dictionary) -> void: + var current = document.duplicate() + document = {} + for key in base_document.keys(): + document[key] = Utilities.to_firebase_type(key) + + var changes = { + "added": [], "removed": [], "updated": [] + } + + for key in current.keys(): + if not document.has(key): + changes.removed.push_back({ "key" : key }) + else: + var new_value = Utilities.from_firebase_type(document[key]) + var old_value = Utilities.from_firebase_type(current[key]) + if typeof(new_value) != typeof(old_value) or new_value != old_value: + changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value }) + + for key in document.keys(): + if not current.has(key): + changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) }) + + if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()): + _emit_changes(changes) -# Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' } -static func fields2dict(doc : Dictionary) -> Dictionary: - var dict : Dictionary = {} - if doc.has("fields"): - for field in (doc.fields).keys(): - if (doc.fields)[field].has("mapValue"): - dict[field] = fields2dict((doc.fields)[field].mapValue) - elif (doc.fields)[field].has("timestampValue"): - dict[field] = timestamp2dict((doc.fields)[field].timestampValue) - elif (doc.fields)[field].has("arrayValue"): - dict[field] = fields2array((doc.fields)[field].arrayValue) - elif (doc.fields)[field].has("integerValue"): - dict[field] = (doc.fields)[field].values()[0] as int - elif (doc.fields)[field].has("doubleValue"): - dict[field] = (doc.fields)[field].values()[0] as float - elif (doc.fields)[field].has("booleanValue"): - dict[field] = (doc.fields)[field].values()[0] as bool - elif (doc.fields)[field].has("nullValue"): - dict[field] = null - else: - dict[field] = (doc.fields)[field].values()[0] - return dict +func is_null_value(key) -> bool: + return document.has(key) and Utilities.from_firebase_type(document[key]) == null -# Pass an Array to parse it to a Firebase arrayValue -static func array2fields(array : Array) -> Array: - var fields : Array = [] - var var_type : String = "" - for field in array: - match typeof(field): - TYPE_DICTIONARY: - if is_field_timestamp(field): - var_type = "timestampValue" - field = dict2timestamp(field) - else: - var_type = "mapValue" - field = dict2fields(field) - TYPE_NIL: var_type = "nullValue" - TYPE_BOOL: var_type = "booleanValue" - TYPE_INT: var_type = "integerValue" - TYPE_REAL: var_type = "doubleValue" - TYPE_STRING: var_type = "stringValue" - TYPE_ARRAY: var_type = "arrayValue" +# As of right now, we do not track these with track changes; instead, they'll come back when the document updates from the server. +# Until that time, it's expected if you want to track these types of changes that you commit for the transforms and then get the document yourself. +func add_field_transform(transform : FieldTransform) -> void: + transform.document_name = doc_name + _transforms.push_back(transform) - fields.append({ var_type : field }) - return fields +func remove_field_transform(transform : FieldTransform) -> void: + _transforms.erase(transform) + +func clear_field_transforms() -> void: + _transforms.transforms.clear() -# Pass a Firebase arrayValue Dictionary to convert it back to an Array -static func fields2array(array : Dictionary) -> Array: - var fields : Array = [] - if array.has("values"): - for field in array.values: - var item - match field.keys()[0]: - "mapValue": - item = fields2dict(field.mapValue) - "arrayValue": - item = fields2array(field.arrayValue) - "integerValue": - item = field.values()[0] as int - "doubleValue": - item = field.values()[0] as float - "booleanValue": - item = field.values()[0] as bool - "timestampValue": - item = timestamp2dict(field.timestampValue) - "nullValue": - item = null - _: - item = field.values()[0] - fields.append(item) - return fields +func remove_field(field_path : String) -> void: + if document.has(field_path): + document[field_path] = Utilities.to_firebase_type(null) + + var changes = { + "added": [], "removed": [], "updated": [] + } + + changes.removed.push_back({ "key" : field_path }) + _emit_changes(changes) + +func _erase(field_path : String) -> void: + document.erase(field_path) -# Converts a gdscript Dictionary (most likely obtained with OS.get_datetime()) to a Firebase Timestamp -static func dict2timestamp(dict : Dictionary) -> String: - dict.erase('weekday') - dict.erase('dst') - var dict_values : Array = dict.values() - return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values +func add_or_update_field(field_path : String, value : Variant) -> void: + var changes = { + "added": [], "removed": [], "updated": [] + } + + var existing_value = get_value(field_path) + var has_field_path = existing_value != null and not is_null_value(field_path) + + var converted_value = Utilities.to_firebase_type(value) + document[field_path] = converted_value + + if has_field_path: + changes.updated.push_back({ "key" : field_path, "old" : existing_value, "new" : value }) + else: + changes.added.push_back({ "key" : field_path, "new" : value }) + + _emit_changes(changes) + +func on_snapshot(when_called : Callable, poll_time : float = 1.0) -> FirestoreListener.FirestoreListenerConnection: + if get_child_count() >= 1: # Only one listener per + assert(false, "Multiple listeners not allowed for the same document yet") + return + + var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate() + add_child(listener) + listener.initialize_listener(collection_name, doc_name) + listener.owner = self + listener.changed.connect(when_called, CONNECT_REFERENCE_COUNTED) + var result = listener.enable_connection() + return result -# Converts a Firebase Timestamp back to a gdscript Dictionary -static func timestamp2dict(timestamp : String) -> Dictionary: - var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} - var dict : PoolStringArray = timestamp.split("T")[0].split("-") - dict.append_array(timestamp.split("T")[1].split(":")) - for value in dict.size() : - datetime[datetime.keys()[value]] = int(dict[value]) - return datetime +func get_value(property : StringName) -> Variant: + if property == "doc_name": + return doc_name + elif property == "collection_name": + return collection_name + elif property == "create_time": + return create_time + + if document.has(property): + var result = Utilities.from_firebase_type(document[property]) + return result + + return null -static func is_field_timestamp(field : Dictionary) -> bool: - return field.has_all(['year','month','day','hour','minute','second']) +func _get(property: StringName) -> Variant: + return get_value(property) + +func _set(property: StringName, value: Variant) -> bool: + assert(value != null, "When using the dictionary setter, the value cannot be null; use erase_field instead.") + document[property] = Utilities.to_firebase_type(value) + return true + +func get_unsafe_document() -> Dictionary: + var result = {} + for key in keys(): + result[key] = Utilities.from_firebase_type(document[key]) + + return result + +func keys(): + return document.keys() # Call print(document) to return directly this document formatted func _to_string() -> String: - return ("doc_name: {doc_name}, \ndoc_fields: {doc_fields}, \ncreate_time: {create_time}\n").format( - {doc_name = self.doc_name, - doc_fields = self.doc_fields, - create_time = self.create_time}) + return ("doc_name: {doc_name}, \ndata: {data}, \ncreate_time: {create_time}\n").format( + {doc_name = self.doc_name, + data = document, + create_time = self.create_time}) diff --git a/addons/godot-firebase/firestore/firestore_document.gd.uid b/addons/godot-firebase/firestore/firestore_document.gd.uid new file mode 100644 index 0000000..646bead --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_document.gd.uid @@ -0,0 +1 @@ +uid://bvk3axe8iggqw diff --git a/addons/godot-firebase/firestore/firestore_listener.gd b/addons/godot-firebase/firestore/firestore_listener.gd new file mode 100644 index 0000000..01c4286 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_listener.gd @@ -0,0 +1,137 @@ +class_name FirestoreListener +extends Node + +signal changed(changes) + +var _doc_name: String +var _collection: FirestoreCollection +var _collection_name: String +var _rtdb_ref: FirebaseDatabaseReference +var _rtdb_path: String +var _last_update_time: float +var _write_ref: FirebaseOnceDatabaseReference # Used for ETag-aware writes + +func initialize_listener(collection_name: String, doc_name: String) -> void: + _doc_name = doc_name + _collection_name = collection_name + _collection = Firebase.Firestore.collection(_collection_name) + _rtdb_path = "firestore_mirrored_listener_data/%s" % _collection_name + _rtdb_ref = Firebase.Database.get_database_reference(_rtdb_path, {}) + + # Create a separate reference for writes to handle ETags + _write_ref = Firebase.Database.get_once_database_reference(_rtdb_path) + + _last_update_time = Time.get_unix_time_from_system() + print("[FirestoreListener] Initialized for %s/%s at RTDB path: %s" % [_collection_name, _doc_name, _rtdb_path]) + _rtdb_ref.patch_data_update.connect(_on_data_updated) + _rtdb_ref.new_data_update.connect(_on_data_updated) + # We might want to handle deletes differently, but for now let's see if they come through + # _rtdb_ref.delete_data_update.connect(_on_data_updated) + +func send_change(changes) -> void: + changes["update_time"] = Time.get_unix_time_from_system() + print("[FirestoreListener] Attempting to send change with ETag sync: %s" % str(changes)) + + # Start the write loop + _attempt_write_with_retry(changes) + +func _attempt_write_with_retry(changes: Dictionary, attempt: int = 1) -> void: + if attempt > 5: + print("[FirestoreListener] Max retry attempts reached. Write failed.") + return + + # 1. Get current state and ETag + _write_ref.once(_doc_name) + var data = await _write_ref.once_successful + var current_etag = _write_ref.last_etag + + # 2. Try to update with ETag + _write_ref.put(_doc_name, changes, current_etag) + + # 3. Wait for result + var result = await _wait_for_write_result() + + if not result: + print("[FirestoreListener] Write failed (likely ETag mismatch). Retrying...") + # Add a small random delay before retry to prevent thundering herd + await _rtdb_ref.get_tree().create_timer(randf_range(0.1, 0.5)).timeout + _attempt_write_with_retry(changes, attempt + 1) + +func _wait_for_write_result() -> bool: + # Wait for either success or failure signal + # Note: update() calls use the pusher, so we listen to push_successful/failed + var success = _write_ref.push_successful + var failure = _write_ref.push_failed + + var multi_signal = Utilities.MultiSignal.new([success, failure]) + var result_signal = await multi_signal.completed + + # Check which signal completed the race + return result_signal == success + + +func _on_data_updated(data: FirebaseResource) -> void: + print("[FirestoreListener] Received data update. Key: %s, Data: %s" % [data.key, str(data.data)]) + # Only process updates for this specific document + if data.key != _doc_name: + print("[FirestoreListener] Ignoring update for different document: %s" % data.key) + return + + # With ETags, we trust the server's state implicitly. + # Any update that made it here passed the ETag check (or was a forced write). + + # Also check wall-clock time as a secondary ordering mechanism + var incoming_update_time = data.data.get("update_time", 0.0) as float + if incoming_update_time > _last_update_time: + _last_update_time = incoming_update_time + + # Fetch the latest document state from Firestore + # Since we are a child of the FirestoreDocument, we can just use it directly. + # We cast to Node first to avoid cyclic dependency issues if not fully loaded, + # but ideally we trust get_parent() is the document. + var document = get_parent() + if not document: + print("[FirestoreListener] Error: Listener has no parent document!") + return + + # Extract change information + var changes = data.data as Dictionary + var updates = changes.get("updated", []) + var deletes = changes.get("removed", []) + var adds = changes.get("added", []) + + # Apply deletions + if deletes: + for delete in deletes: + document._erase(delete.key) + + # Apply additions + if adds: + for add in adds: + document[add.key] = add.new + + # Apply updates + if updates: + for update in updates: + document[update.key] = update.new + + # Emit the changes to any connected listeners + changed.emit(changes) + + +func enable_connection() -> FirestoreListenerConnection: + return FirestoreListenerConnection.new(self) + +class FirestoreListenerConnection extends RefCounted: + var connection + + func _init(connection_node): + connection = connection_node + + func stop(): + if connection != null and is_instance_valid(connection): + connection.free() + + func send_change(changes): + if connection != null and is_instance_valid(connection): + connection.send_change(changes) diff --git a/addons/godot-firebase/firestore/firestore_listener.gd.uid b/addons/godot-firebase/firestore/firestore_listener.gd.uid new file mode 100644 index 0000000..e8a1a35 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_listener.gd.uid @@ -0,0 +1 @@ +uid://c012g87mllspa diff --git a/addons/godot-firebase/firestore/firestore_listener.tscn b/addons/godot-firebase/firestore/firestore_listener.tscn new file mode 100644 index 0000000..db467d4 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_listener.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bwv7vtgssc0n5"] + +[ext_resource type="Script" uid="uid://c012g87mllspa" path="res://addons/godot-firebase/firestore/firestore_listener.gd" id="1_qlaei"] + +[node name="FirestoreListener" type="Node"] +script = ExtResource("1_qlaei") diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index a75a692..bdebbe6 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -1,105 +1,101 @@ -## @meta-authors Nicoló 'fenix' Santilio +## @meta-authors Nicoló 'fenix' Santilio, Kyle Szklenski ## @meta-version 1.4 ## A firestore query. ## Documentation TODO. -tool -extends Reference +@tool +extends RefCounted class_name FirestoreQuery class Order: - var obj : Dictionary + var obj: Dictionary class Cursor: - var values : Array - var before : bool + var values: Array + var before: bool - func _init(v : Array, b : bool): - values = v - before = b + func _init(v : Array,b : bool): + values = v + before = b signal query_result(query_result) -const TEMPLATE_QUERY : Dictionary = { - select = {}, - from = [], - where = {}, - orderBy = [], - startAt = {}, - endAt = {}, - offset = 0, - limit = 0 +const TEMPLATE_QUERY: Dictionary = { + select = {}, + from = [], + where = {}, + orderBy = [], + startAt = {}, + endAt = {}, + offset = 0, + limit = 0 } -var query : Dictionary = {} +var query: Dictionary = {} +var aggregations: Array[Dictionary] = [] enum OPERATOR { - # Standard operators - OPERATOR_NSPECIFIED, - LESS_THAN, - LESS_THAN_OR_EQUAL, - GREATER_THAN, - GREATER_THAN_OR_EQUAL, - EQUAL, - NOT_EQUAL, - ARRAY_CONTAINS, - ARRAY_CONTAINS_ANY, - IN, - NOT_IN, - - # Unary operators - IS_NAN, - IS_NULL, - IS_NOT_NAN, - IS_NOT_NULL, - - # Complex operators - AND, - OR + # Standard operators + OPERATOR_UNSPECIFIED, + LESS_THAN, + LESS_THAN_OR_EQUAL, + GREATER_THAN, + GREATER_THAN_OR_EQUAL, + EQUAL, + NOT_EQUAL, + ARRAY_CONTAINS, + ARRAY_CONTAINS_ANY, + IN, + NOT_IN, + + # Unary operators + IS_NAN, + IS_NULL, + IS_NOT_NAN, + IS_NOT_NULL, + + # Complex operators + AND, + OR } enum DIRECTION { - DIRECTION_UNSPECIFIED, - ASCENDING, - DESCENDING + DIRECTION_UNSPECIFIED, + ASCENDING, + DESCENDING } -func _init(): - return self - # Select which fields you want to return as a reflection from your query. # Fields must be added inside a list. Only a field is accepted inside the list # Leave the Array empty if you want to return the whole document func select(fields) -> FirestoreQuery: - match typeof(fields): - TYPE_STRING: - query["select"] = { fields = { fieldPath = fields } } - TYPE_ARRAY: - for field in fields: - field = ({ fieldPath = field }) - query["select"] = { fields = fields } - _: - print("Type of 'fields' is not accepted.") - return self + match typeof(fields): + TYPE_STRING: + query["select"] = { fields = { fieldPath = fields } } + TYPE_ARRAY: + for field in fields: + field = ({ fieldPath = field }) + query["select"] = { fields = fields } + _: + print("Type of 'fields' is not accepted.") + return self # Select the collection you want to return the query result from # if @all_descendants also sub-collections will be returned. If false, only documents will be returned func from(collection_id : String, all_descendants : bool = true) -> FirestoreQuery: - query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}] - return self - - + query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}] + return self # @collections_array MUST be an Array of Arrays with this structure # [ ["collection_id", true/false] ] func from_many(collections_array : Array) -> FirestoreQuery: - var collections : Array = [] - for collection in collections_array: - collections.append({collectionId = collection[0], allDescendants = collection[1]}) - query["from"] = collections.duplicate(true) - return self + var collections : Array = [] + for collection in collections_array: + collections.append({collectionId = collection[0], allDescendants = collection[1]}) + query["from"] = collections.duplicate(true) + return self # Query the value of a field you want to match @@ -107,45 +103,45 @@ func from_many(collections_array : Array) -> FirestoreQuery: # @operator : from FirestoreQuery.OPERATOR # @value : can be any type - String, int, bool, float # @chain : from FirestoreQuery.OPERATOR.[OR/AND], use it only if you want to chain "AND" or "OR" logic with futher where() calls -# eg. .where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20) +# eg. super.where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20) func where(field : String, operator : int, value = null, chain : int = -1): - if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: - if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): - var filters : Array = [] - if query.has("where") and query.where.has("compositeFilter"): - if chain == -1: - filters = query.where.compositeFilter.filters.duplicate(true) - chain = OPERATOR.get(query.where.compositeFilter.op) - else: - filters.append(query.where) - filters.append(create_unary_filter(field, operator)) - query["where"] = create_composite_filter(chain, filters) - else: - query["where"] = create_unary_filter(field, operator) - else: - if value == null: - print("A value must be defined to match the field: {field}".format({field = field})) - else: - if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): - var filters : Array = [] - if query.has("where") and query.where.has("compositeFilter"): - if chain == -1: - filters = query.where.compositeFilter.filters.duplicate(true) - chain = OPERATOR.get(query.where.compositeFilter.op) - else: - filters.append(query.where) - filters.append(create_field_filter(field, operator, value)) - query["where"] = create_composite_filter(chain, filters) - else: - query["where"] = create_field_filter(field, operator, value) - return self + if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: + if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): + var filters : Array = [] + if query.has("where") and query.where.has("compositeFilter"): + if chain == -1: + filters = query.where.compositeFilter.filters.duplicate(true) + chain = OPERATOR.get(query.where.compositeFilter.op) + else: + filters.append(query.where) + filters.append(create_unary_filter(field, operator)) + query["where"] = create_composite_filter(chain, filters) + else: + query["where"] = create_unary_filter(field, operator) + else: + if value == null: + print("A value must be defined to match the field: {field}".format({field = field})) + else: + if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): + var filters : Array = [] + if query.has("where") and query.where.has("compositeFilter"): + if chain == -1: + filters = query.where.compositeFilter.filters.duplicate(true) + chain = OPERATOR.get(query.where.compositeFilter.op) + else: + filters.append(query.where) + filters.append(create_field_filter(field, operator, value)) + query["where"] = create_composite_filter(chain, filters) + else: + query["where"] = create_field_filter(field, operator, value) + return self # Order by a field, defining its name and the order direction # default directoin = Ascending func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> FirestoreQuery: - query["orderBy"] = [_order_object(field, direction).obj] - return self + query["orderBy"] = [_order_object(field, direction).obj] + return self # Order by a set of fields and directions @@ -153,88 +149,106 @@ func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> Firestor # [@field_name , @DIRECTION.[direction]] # else, order_object() can be called to return an already parsed Dictionary func order_by_fields(order_field_list : Array) -> FirestoreQuery: - var order_list : Array = [] - for order in order_field_list: - if order is Array: - order_list.append(_order_object(order[0], order[1]).obj) - elif order is Order: - order_list.append(order.obj) - query["orderBy"] = order_list - return self - - + var order_list : Array = [] + for order in order_field_list: + if order is Array: + order_list.append(_order_object(order[0], order[1]).obj) + elif order is Order: + order_list.append(order.obj) + query["orderBy"] = order_list + return self func start_at(value, before : bool) -> FirestoreQuery: - var cursor : Cursor = _cursor_object(value, before) - query["startAt"] = { values = cursor.values, before = cursor.before } - print(query["startAt"]) - return self + var cursor : Cursor = _cursor_object(value, before) + query["startAt"] = { values = cursor.values, before = cursor.before } + print(query["startAt"]) + return self func end_at(value, before : bool) -> FirestoreQuery: - var cursor : Cursor = _cursor_object(value, before) - query["startAt"] = { values = cursor.values, before = cursor.before } - print(query["startAt"]) - return self + var cursor : Cursor = _cursor_object(value, before) + query["startAt"] = { values = cursor.values, before = cursor.before } + print(query["startAt"]) + return self func offset(offset : int) -> FirestoreQuery: - if offset < 0: - print("If specified, offset must be >= 0") - else: - query["offset"] = offset - return self + if offset < 0: + print("If specified, offset must be >= 0") + else: + query["offset"] = offset + return self func limit(limit : int) -> FirestoreQuery: - if limit < 0: - print("If specified, offset must be >= 0") - else: - query["limit"] = limit - return self - - + if limit < 0: + print("If specified, offset must be >= 0") + else: + query["limit"] = limit + return self + + +func aggregate() -> FirestoreAggregation: + return FirestoreAggregation.new(self) + +class FirestoreAggregation extends RefCounted: + var _query: FirestoreQuery + + func _init(query: FirestoreQuery) -> void: + _query = query + + func sum(field: String) -> FirestoreQuery: + _query.aggregations.push_back({ sum = { field = { fieldPath = field }}}) + return _query + + func count(up_to: int) -> FirestoreQuery: + _query.aggregations.push_back({ count = { upTo = up_to }}) + return _query + + func average(field: String) -> FirestoreQuery: + _query.aggregations.push_back({ avg = { field = { fieldPath = field }}}) + return _query # UTILITIES ---------------------------------------- static func _cursor_object(value, before : bool) -> Cursor: - var parse : Dictionary = FirestoreDocument.dict2fields({value = value}).fields.value - var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before) - return cursor + var parse : Dictionary = Utilities.dict2fields({value = value}).fields.value + var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before) + return cursor static func _order_object(field : String, direction : int) -> Order: - var order : Order = Order.new() - order.obj = { field = { fieldPath = field }, direction = DIRECTION.keys()[direction] } - return order + var order : Order = Order.new() + order.obj = { field = { fieldPath = field }, direction = DIRECTION.keys()[direction] } + return order func create_field_filter(field : String, operator : int, value) -> Dictionary: - return { - fieldFilter = { - field = { fieldPath = field }, - op = OPERATOR.keys()[operator], - value = FirestoreDocument.dict2fields({value = value}).fields.value - } } + return { + fieldFilter = { + field = { fieldPath = field }, + op = OPERATOR.keys()[operator], + value = Utilities.dict2fields({value = value}).fields.value + } } func create_unary_filter(field : String, operator : int) -> Dictionary: - return { - unaryFilter = { - field = { fieldPath = field }, - op = OPERATOR.keys()[operator], - } } + return { + unaryFilter = { + field = { fieldPath = field }, + op = OPERATOR.keys()[operator], + } } func create_composite_filter(operator : int, filters : Array) -> Dictionary: - return { - compositeFilter = { - op = OPERATOR.keys()[operator], - filters = filters - } } + return { + compositeFilter = { + op = OPERATOR.keys()[operator], + filters = filters + } } func clean() -> void: - query = { } + query = { } func _to_string() -> String: - var pretty : String = "QUERY:\n" - for key in query.keys(): - pretty += "- {key} = {value}\n".format({key = key, value = query.get(key)}) - return pretty + var pretty : String = "QUERY:\n" + for key in query.keys(): + pretty += "- {key} = {value}\n".format({key = key, value = query.get(key)}) + return pretty diff --git a/addons/godot-firebase/firestore/firestore_query.gd.uid b/addons/godot-firebase/firestore/firestore_query.gd.uid new file mode 100644 index 0000000..55c7863 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_query.gd.uid @@ -0,0 +1 @@ +uid://tvsa0vdxdvo2 diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index ad65f03..8122ae5 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -4,369 +4,185 @@ ## A [code]FirestoreTask[/code] is an independent node inheriting [code]HTTPRequest[/code] that processes a [code]Firestore[/code] request. ## Once the Task is completed (both if successfully or not) it will emit the relative signal (or a general purpose signal [code]task_finished()[/code]) and will destroy automatically. ## -## Being a [code]Node[/code] it can be stored in a variable to yield on it, and receive its result as a callback. +## Being a [code]Node[/code] it can be stored in a variable to yield checked it, and receive its result as a callback. ## All signals emitted by a [code]FirestoreTask[/code] represent a direct level of signal communication, which can be high ([code]get_document(document), result_query(result)[/code]) or low ([code]task_finished(result)[/code]). ## An indirect level of communication with Tasks is also provided, redirecting signals to the [class FirebaseFirestore] module. ## ## ex. ## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code] -## [code]var result : Array = yield(task, "task_finished")[/code] -## [code]var result : Array = yield(task, "result_query")[/code] -## [code]var result : Array = yield(Firebase.Firestore, "task_finished")[/code] -## [code]var result : Array = yield(Firebase.Firestore, "result_query")[/code] +## [code]var result : Array = await task.task_finished[/code] +## [code]var result : Array = await task.result_query[/code] +## [code]var result : Array = await Firebase.Firestore.task_finished[/code] +## [code]var result : Array = await Firebase.Firestore.result_query[/code] ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask -tool +@tool class_name FirestoreTask -extends Reference +extends RefCounted ## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result. ## @arg-types Variant -signal task_finished(task) -## Emitted when a [code]add(document)[/code] request on a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types FirestoreDocument -signal add_document(doc) -## Emitted when a [code]get(document)[/code] request on a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types FirestoreDocument -signal get_document(doc) -## Emitted when a [code]update(document)[/code] request on a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types FirestoreDocument -signal update_document(doc) -## Emitted when a [code]delete(document)[/code] request on a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types FirestoreDocument -signal delete_document() -## Emitted when a [code]list(collection_id)[/code] request on [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types Array -signal listed_documents(documents) -## Emitted when a [code]query(collection_id)[/code] request on [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. -## @arg-types Array -signal result_query(result) -## Emitted when a request is [b]not[/b] successfully completed. -## @arg-types Dictionary -signal task_error(code, status, message, task) +signal task_finished() enum Task { - TASK_GET, ## A GET Request Task, processing a get() request - TASK_POST, ## A POST Request Task, processing add() request - TASK_PATCH, ## A PATCH Request Task, processing a update() request - TASK_DELETE, ## A DELETE Request Task, processing a delete() request - TASK_QUERY, ## A POST Request Task, processing a query() request - TASK_LIST ## A POST Request Task, processing a list() request + TASK_GET, ## A GET Request Task, processing a get() request + TASK_POST, ## A POST Request Task, processing add() request + TASK_PATCH, ## A PATCH Request Task, processing a update() request + TASK_DELETE, ## A DELETE Request Task, processing a delete() request + TASK_QUERY, ## A POST Request Task, processing a query() request + TASK_AGG_QUERY, ## A POST Request Task, processing an aggregation_query() request + TASK_LIST, ## A POST Request Task, processing a list() request + TASK_COMMIT ## A POST Request Task that hits the write api +} + +## Mapping of Task enum values to descriptions for use in printing user-friendly error codes. +const TASK_MAP = { + Task.TASK_GET: "GET DOCUMENT", + Task.TASK_POST: "ADD DOCUMENT", + Task.TASK_PATCH: "UPDATE DOCUMENT", + Task.TASK_DELETE: "DELETE DOCUMENT", + Task.TASK_QUERY: "QUERY COLLECTION", + Task.TASK_LIST: "LIST DOCUMENTS", + Task.TASK_COMMIT: "COMMIT DOCUMENT", + Task.TASK_AGG_QUERY: "AGG QUERY COLLECTION" } ## The code indicating the request Firestore is processing. ## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers. ## @setter set_action -var action : int = -1 setget set_action +var action : int = -1 : set = set_action ## A variable, temporary holding the result of the request. var data -var error : Dictionary -var document : FirestoreDocument -## Whether the data came from cache. -var from_cache : bool = false - -var _response_headers : PoolStringArray = PoolStringArray() -var _response_code : int = 0 - -var _method : int = -1 -var _url : String = "" -var _fields : String = "" -var _headers : PoolStringArray = [] - -#func _ready() -> void: -# connect("request_completed", self, "_on_request_completed") - - -#func _push_request(url := "", headers := "", fields := "") -> void: -# _url = url -# _fields = fields -# var temp_header : Array = [] -# temp_header.append(headers) -# _headers = PoolStringArray(temp_header) -# -# if Firebase.Firestore._offline: -# call_deferred("_on_request_completed", -1, 404, PoolStringArray(), PoolByteArray()) -# else: -# request(_url, _headers, true, _method, _fields) - - -func _on_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - var bod - if validate_json(body.get_string_from_utf8()).empty(): - bod = JSON.parse(body.get_string_from_utf8()).result - - var offline: bool = typeof(bod) == TYPE_NIL - var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK - from_cache = offline - - Firebase.Firestore._set_offline(offline) - - var cache_path : String = Firebase._config["cacheLocation"] - if not cache_path.empty() and not failed and Firebase.Firestore.persistence_enabled: - var encrypt_key: String = Firebase.Firestore._encrypt_key - var full_path : String - var url_segment : String - match action: - Task.TASK_LIST: - url_segment = data[0] - full_path = cache_path - Task.TASK_QUERY: - url_segment = JSON.print(data.query) - full_path = cache_path - _: - url_segment = to_json(data) - full_path = _get_doc_file(cache_path, url_segment, encrypt_key) - bod = _handle_cache(offline, data, encrypt_key, full_path, bod) - if not bod.empty() and offline: - response_code = HTTPClient.RESPONSE_OK - - if response_code == HTTPClient.RESPONSE_OK: - data = bod - match action: - Task.TASK_POST: - document = FirestoreDocument.new(bod) - emit_signal("add_document", document) - Task.TASK_GET: - document = FirestoreDocument.new(bod) - emit_signal("get_document", document) - Task.TASK_PATCH: - document = FirestoreDocument.new(bod) - emit_signal("update_document", document) - Task.TASK_DELETE: - emit_signal("delete_document") - Task.TASK_QUERY: - data = [] - for doc in bod: - if doc.has('document'): - data.append(FirestoreDocument.new(doc.document)) - emit_signal("result_query", data) - Task.TASK_LIST: - data = [] - if bod.has('documents'): - for doc in bod.documents: - data.append(FirestoreDocument.new(doc)) - if bod.has("nextPageToken"): - data.append(bod.nextPageToken) - emit_signal("listed_documents", data) - else: - Firebase._printerr("Action in error was: " + str(action)) - emit_error("task_error", bod, action) - - emit_signal("task_finished", self) - -func emit_error(signal_name : String, bod, task) -> void: - if bod: - if bod is Array and bod.size() > 0 and bod[0].has("error"): - error = bod[0].error - elif bod is Dictionary and bod.keys().size() > 0 and bod.has("error"): - error = bod.error - - emit_signal(signal_name, error.code, error.status, error.message, task) - - return - - emit_signal(signal_name, 1, 0, "Unknown error", task) +var error: Dictionary +var document: FirestoreDocument + +var _response_headers: PackedStringArray = PackedStringArray() +var _response_code: int = 0 + +var _method: int = -1 +var _url: String = "" +var _fields: String = "" +var _headers: PackedStringArray = [] + +func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: + var bod = body.get_string_from_utf8() + if bod != "": + bod = Utilities.get_json_data(bod) + + var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK + # Probably going to regret this... + if response_code == HTTPClient.RESPONSE_OK: + match action: + Task.TASK_POST, Task.TASK_GET, Task.TASK_PATCH: + document = FirestoreDocument.new(bod) + data = document + Task.TASK_DELETE: + data = true + Task.TASK_QUERY: + data = [] + for doc in bod: + if doc.has('document'): + data.append(FirestoreDocument.new(doc.document)) + Task.TASK_AGG_QUERY: + var agg_results = [] + for agg_result in bod: + var idx = 0 + var query_results = {} + for field_value in agg_result.result.aggregateFields.keys(): + var agg = data.aggregations[idx] + var field = agg_result.result.aggregateFields[field_value] + query_results[agg.keys()[0]] = Utilities.from_firebase_type(field) + idx += 1 + agg_results.push_back(query_results) + data = agg_results + Task.TASK_LIST: + data = [] + if bod.has('documents'): + for doc in bod.documents: + data.append(FirestoreDocument.new(doc)) + if bod.has("nextPageToken"): + data.append(bod.nextPageToken) + Task.TASK_COMMIT: + data = bod # Commit's response is not a full document, so don't treat it as such + else: + var description = "" + if TASK_MAP.has(action): + description = "(" + TASK_MAP[action] + ")" + + Firebase._printerr("Action in error was: " + str(action) + " " + description) + build_error(bod, action, description) + + task_finished.emit() + +func build_error(_error, action, description) -> void: + if _error: + if _error is Array and _error.size() > 0 and _error[0].has("error"): + _error = _error[0].error + elif _error is Dictionary and _error.keys().size() > 0 and _error.has("error"): + _error = _error.error + + error = _error + else: + #error.code, error.status, error.message + error = { "error": { + "code": 0, + "status": "Unknown Error", + "message": "Error: %s - %s" % [action, description] + } + } + + data = null func set_action(value : int) -> void: - action = value - match action: - Task.TASK_GET, Task.TASK_LIST: - _method = HTTPClient.METHOD_GET - Task.TASK_POST, Task.TASK_QUERY: - _method = HTTPClient.METHOD_POST - Task.TASK_PATCH: - _method = HTTPClient.METHOD_PATCH - Task.TASK_DELETE: - _method = HTTPClient.METHOD_DELETE - - -func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: - var body_return := {} - - var dir := Directory.new() - dir.make_dir_recursive(cache_path) - var file := File.new() - match action: - Task.TASK_POST: - if offline: - var save: Dictionary - if offline: - save = { - "name": "projects/%s/databases/(default)/documents/%s" % [Firebase._config["storageBucket"], data], - "fields": JSON.parse(_fields).result["fields"], - "createTime": "from_cache_file", - "updateTime": "from_cache_file" - } - else: - save = body.duplicate() - - if file.open_encrypted_with_pass(cache_path, File.READ, encrypt_key) == OK: - file.store_line(data) - file.store_line(JSON.print(save)) - body_return = save - else: - Firebase._printerr("Error saving cache file! Error code: %d" % file.get_error()) - file.close() - - Task.TASK_PATCH: - if offline: - var save := { - "fields": {} - } - if offline: - var mod: Dictionary - mod = { - "name": "projects/%s/databases/(default)/documents/%s" % [Firebase._config["storageBucket"], data], - "fields": JSON.parse(_fields).result["fields"], - "createTime": "from_cache_file", - "updateTime": "from_cache_file" - } - - if file.file_exists(cache_path): - if file.open_encrypted_with_pass(cache_path, File.READ, encrypt_key) == OK: - if file.get_len(): - assert(data == file.get_line()) - var content := file.get_line() - if content != "--deleted--": - save = JSON.parse(content).result - else: - Firebase._printerr("Error updating cache file! Error code: %d" % file.get_error()) - file.close() - - save.fields = FirestoreDocument.dict2fields(_merge_dict( - FirestoreDocument.fields2dict({"fields": save.fields}), - FirestoreDocument.fields2dict({"fields": mod.fields}), - not offline - )).fields - save.name = mod.name - save.createTime = mod.createTime - save.updateTime = mod.updateTime - else: - save = body.duplicate() - - - if file.open_encrypted_with_pass(cache_path, File.WRITE, encrypt_key) == OK: - file.store_line(data) - file.store_line(JSON.print(save)) - body_return = save - else: - Firebase._printerr("Error updating cache file! Error code: %d" % file.get_error()) - file.close() - - Task.TASK_GET: - if offline and file.file_exists(cache_path): - if file.open_encrypted_with_pass(cache_path, File.READ, encrypt_key) == OK: - assert(data == file.get_line()) - var content := file.get_line() - if content != "--deleted--": - body_return = JSON.parse(content).result - else: - Firebase._printerr("Error reading cache file! Error code: %d" % file.get_error()) - file.close() - - Task.TASK_DELETE: - if offline: - if file.open_encrypted_with_pass(cache_path, File.WRITE, encrypt_key) == OK: - file.store_line(data) - file.store_line("--deleted--") - body_return = {"deleted": true} - else: - Firebase._printerr("Error \"deleting\" cache file! Error code: %d" % file.get_error()) - file.close() - else: - dir.remove(cache_path) - - Task.TASK_LIST: - if offline: - var cache_dir := Directory.new() - var cache_files := [] - if cache_dir.open(cache_path) == OK: - cache_dir.list_dir_begin(true) - var file_name = cache_dir.get_next() - while file_name != "": - if not cache_dir.current_is_dir() and file_name.ends_with(Firebase.Firestore._CACHE_EXTENSION): - cache_files.append(cache_path.plus_file(file_name)) - file_name = cache_dir.get_next() - cache_dir.list_dir_end() - cache_files.erase(cache_path.plus_file(Firebase.Firestore._CACHE_RECORD_FILE)) - cache_dir.remove(cache_path.plus_file(Firebase.Firestore._CACHE_RECORD_FILE)) - print(cache_files) - - body_return.documents = [] - for cache in cache_files: - if file.open_encrypted_with_pass(cache, File.READ, encrypt_key) == OK: - if file.get_line().begins_with(data[0]): - body_return.documents.append(JSON.parse(file.get_line()).result) - else: - Firebase._printerr("Error opening cache file for listing! Error code: %d" % file.get_error()) - file.close() - body_return.documents.resize(min(data[1], body_return.documents.size())) - body_return.nextPageToken = "" - - Task.TASK_QUERY: - if offline: - Firebase._printerr("Offline queries are currently unsupported!") - - if not offline: - return body - else: - return body_return + action = value + match action: + Task.TASK_GET, Task.TASK_LIST: + _method = HTTPClient.METHOD_GET + Task.TASK_POST, Task.TASK_QUERY, Task.TASK_AGG_QUERY: + _method = HTTPClient.METHOD_POST + Task.TASK_PATCH: + _method = HTTPClient.METHOD_PATCH + Task.TASK_DELETE: + _method = HTTPClient.METHOD_DELETE + Task.TASK_COMMIT: + _method = HTTPClient.METHOD_POST + _: + assert(false) func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: - var ret := dic_a.duplicate(true) - for key in dic_b: - var val = dic_b[key] + var ret := dic_a.duplicate(true) + for key in dic_b: + var val = dic_b[key] - if val == null and nullify: - ret.erase(key) - elif val is Array: - ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val) - elif val is Dictionary: - ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val) - else: - ret[key] = val - return ret + if val == null and nullify: + ret.erase(key) + elif val is Array: + ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val) + elif val is Dictionary: + ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val) + else: + ret[key] = val + return ret func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: - var ret := arr_a.duplicate(true) - ret.resize(len(arr_b)) - - var deletions := 0 - for i in len(arr_b): - var index : int = i - deletions - var val = arr_b[index] - if val == null and nullify: - ret.remove(index) - deletions += i - elif val is Array: - ret[index] = _merge_array(ret[index] if ret[index] else [], val) - elif val is Dictionary: - ret[index] = _merge_dict(ret[index] if ret[index] else {}, val) - else: - ret[index] = val - return ret - - -static func _get_doc_file(cache_path : String, document_id : String, encrypt_key : String) -> String: - var file := File.new() - var path := "" - var i = 0 - while i < 256: - path = cache_path.plus_file("%s-%d.fscache" % [str(document_id.hash()).pad_zeros(10), i]) - if file.file_exists(path): - var is_file := false - if file.open_encrypted_with_pass(path, File.READ, encrypt_key) == OK: - is_file = file.get_line() == document_id - file.close() - - if is_file: - return path - else: - i += 1 - else: - return path - return path + var ret := arr_a.duplicate(true) + ret.resize(len(arr_b)) + + var deletions := 0 + for i in len(arr_b): + var index : int = i - deletions + var val = arr_b[index] + if val == null and nullify: + ret.remove_at(index) + deletions += i + elif val is Array: + ret[index] = _merge_array(ret[index] if ret[index] else [], val) + elif val is Dictionary: + ret[index] = _merge_dict(ret[index] if ret[index] else {}, val) + else: + ret[index] = val + return ret diff --git a/addons/godot-firebase/firestore/firestore_task.gd.uid b/addons/godot-firebase/firestore/firestore_task.gd.uid new file mode 100644 index 0000000..17bee30 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_task.gd.uid @@ -0,0 +1 @@ +uid://cotr2njft8kti diff --git a/addons/godot-firebase/firestore/firestore_transform.gd b/addons/godot-firebase/firestore/firestore_transform.gd new file mode 100644 index 0000000..6de6597 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_transform.gd @@ -0,0 +1,3 @@ +class_name FirestoreTransform +extends RefCounted + diff --git a/addons/godot-firebase/firestore/firestore_transform.gd.uid b/addons/godot-firebase/firestore/firestore_transform.gd.uid new file mode 100644 index 0000000..f69b483 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_transform.gd.uid @@ -0,0 +1 @@ +uid://c43gtlu3j0m4h diff --git a/addons/godot-firebase/functions/function_task.gd b/addons/godot-firebase/functions/function_task.gd index f82e4dd..96a6222 100644 --- a/addons/godot-firebase/functions/function_task.gd +++ b/addons/godot-firebase/functions/function_task.gd @@ -3,16 +3,16 @@ ## ## ex. ## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code] -## [code]var result : Array = yield(task, "task_finished")[/code] -## [code]var result : Array = yield(task, "result_query")[/code] -## [code]var result : Array = yield(Firebase.Firestore, "task_finished")[/code] -## [code]var result : Array = yield(Firebase.Firestore, "result_query")[/code] +## [code]var result : Array = await task.task_finished[/code] +## [code]var result : Array = await task.result_query[/code] +## [code]var result : Array = await Firebase.Firestore.task_finished[/code] +## [code]var result : Array = await Firebase.Firestore.result_query[/code] ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask -tool +@tool class_name FunctionTask -extends Reference +extends RefCounted ## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result. ## @arg-types Variant @@ -33,39 +33,27 @@ var error: Dictionary ## Whether the data came from cache. var from_cache : bool = false -var _response_headers : PoolStringArray = PoolStringArray() +var _response_headers : PackedStringArray = PackedStringArray() var _response_code : int = 0 var _method : int = -1 var _url : String = "" var _fields : String = "" -var _headers : PoolStringArray = [] +var _headers : PackedStringArray = [] -func _on_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - var bod - if validate_json(body.get_string_from_utf8()).empty(): - bod = JSON.parse(body.get_string_from_utf8()).result - else: - bod = {content = body.get_string_from_utf8()} +func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + var bod = Utilities.get_json_data(body) + if bod == null: + bod = {content = body.get_string_from_utf8()} # I don't understand what this line does at all. What the hell?! - var offline: bool = typeof(bod) == TYPE_NIL - from_cache = offline + var offline: bool = typeof(bod) == TYPE_NIL + from_cache = offline - data = bod - if response_code == HTTPClient.RESPONSE_OK and data!=null: - emit_signal("function_executed", result, data) - else: - error = {result=result, response_code=response_code, data=data} - emit_signal("task_error", result, response_code, str(data)) + data = bod + if response_code == HTTPClient.RESPONSE_OK and data!=null: + function_executed.emit(result, data) + else: + error = {result=result, response_code=response_code, data=data} + task_error.emit(result, response_code, str(data)) - emit_signal("task_finished", data) - -# -#func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: -# if offline: -# Firebase._printerr("Offline queries are currently unsupported!") -# -# if not offline: -# return body -# else: -# return body_return + task_finished.emit(data) diff --git a/addons/godot-firebase/functions/function_task.gd.uid b/addons/godot-firebase/functions/function_task.gd.uid new file mode 100644 index 0000000..ece7d30 --- /dev/null +++ b/addons/godot-firebase/functions/function_task.gd.uid @@ -0,0 +1 @@ +uid://dpk0tfg5nj1ky diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index 18fbb96..8ccfa97 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -4,7 +4,7 @@ ## (source: [url=https://firebase.google.com/docs/functions]Functions[/url]) ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Functions -tool +@tool class_name FirebaseFunctions extends Node @@ -30,11 +30,11 @@ var request : int = -1 ## Whether cache files can be used and generated. ## @default true -var persistence_enabled : bool = true +var persistence_enabled : bool = false ## Whether an internet connection can be used. ## @default true -var networking: bool = true setget set_networking +var networking: bool = true : set = set_networking ## A Dictionary containing all authentication fields for the current logged user. ## @type Dictionary @@ -42,176 +42,178 @@ var auth : Dictionary var _config : Dictionary = {} var _cache_loc: String -var _encrypt_key: String = "" if OS.get_name() in ["HTML5", "UWP"] else OS.get_unique_id() +var _encrypt_key: String = "" if Utilities.is_web() else OS.get_unique_id() var _base_url : String = "" var _http_request_pool : Array = [] -var _offline: bool = false setget _set_offline +var _offline: bool = false : set = _set_offline func _ready() -> void: - pass + set_process(false) func _process(delta : float) -> void: - for i in range(_http_request_pool.size() - 1, -1, -1): - var request = _http_request_pool[i] - if not request.get_meta("requesting"): - var lifetime: float = request.get_meta("lifetime") + delta - if lifetime > _MAX_POOLED_REQUEST_AGE: - request.queue_free() - _http_request_pool.remove(i) - request.set_meta("lifetime", lifetime) + for i in range(_http_request_pool.size() - 1, -1, -1): + var request = _http_request_pool[i] + if not request.get_meta("requesting"): + var lifetime: float = request.get_meta("lifetime") + delta + if lifetime > _MAX_POOLED_REQUEST_AGE: + request.queue_free() + _http_request_pool.remove_at(i) + return # Prevent setting a value on request after it's already been queue_freed + request.set_meta("lifetime", lifetime) ## @args ## @return FunctionTask func execute(function: String, method: int, params: Dictionary = {}, body: Dictionary = {}) -> FunctionTask: - var function_task : FunctionTask = FunctionTask.new() - function_task.connect("task_error", self, "_on_task_error") - function_task.connect("task_finished", self, "_on_task_finished") - function_task.connect("function_executed", self, "_on_function_executed") + set_process(true) + var function_task : FunctionTask = FunctionTask.new() + function_task.task_error.connect(_on_task_error) + function_task.task_finished.connect(_on_task_finished) + function_task.function_executed.connect(_on_function_executed) - function_task._method = method + function_task._method = method - var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function - function_task._url = url + var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function + function_task._url = url - if not params.empty(): - url += "?" - for key in params.keys(): - url += key + "=" + params[key] + "&" + if not params.is_empty(): + url += "?" + for key in params.keys(): + url += key + "=" + params[key] + "&" - if not body.empty(): - function_task._headers = PoolStringArray(["Content-Type: application/json"]) - function_task._fields = to_json(body) + if not body.is_empty(): + function_task._fields = JSON.stringify(body) - _pooled_request(function_task) - return function_task + _pooled_request(function_task) + return function_task func set_networking(value: bool) -> void: - if value: - enable_networking() - else: - disable_networking() + if value: + enable_networking() + else: + disable_networking() func enable_networking() -> void: - if networking: - return - networking = true - _base_url = _base_url.replace("storeoffline", "functions") + if networking: + return + networking = true + _base_url = _base_url.replace("storeoffline", "functions") func disable_networking() -> void: - if not networking: - return - networking = false - # Pointing to an invalid url should do the trick. - _base_url = _base_url.replace("functions", "storeoffline") + if not networking: + return + networking = false + # Pointing to an invalid url should do the trick. + _base_url = _base_url.replace("functions", "storeoffline") func _set_offline(value: bool) -> void: - if value == _offline: - return + if value == _offline: + return - _offline = value - if not persistence_enabled: - return + _offline = value + if not persistence_enabled: + return - return + return func _set_config(config_json : Dictionary) -> void: - _config = config_json - _cache_loc = _config["cacheLocation"] + _config = config_json + _cache_loc = _config["cacheLocation"] - if _encrypt_key == "": _encrypt_key = _config.apiKey - _check_emulating() + if _encrypt_key == "": _encrypt_key = _config.apiKey + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://{zone}-{projectId}.cloudfunctions.net/".format({ zone = _config.functionsGeoZone, projectId = _config.projectId }) - else: - var port : String = _config.emulators.ports.functions - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Cloud Functions has not been configured.") - else: - _base_url = "http://localhost:{port}/{projectId}/{zone}/".format({ port = port, zone = _config.functionsGeoZone, projectId = _config.projectId }) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://{zone}-{projectId}.cloudfunctions.net/".format({ zone = _config.functionsGeoZone, projectId = _config.projectId }) + else: + var port : String = _config.emulators.ports.functions + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Cloud Functions has not been configured.") + else: + _base_url = "http://localhost:{port}/{projectId}/{zone}/".format({ port = port, zone = _config.functionsGeoZone, projectId = _config.projectId }) func _pooled_request(task : FunctionTask) -> void: - if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) - return + if _offline: + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) + return - if not auth: - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") - if result[0] != 1: - _check_auth_error(result[0], result[1]) - Firebase._print("Client connected as Anonymous") + if auth == null or auth.is_empty(): + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = await Firebase.Auth.auth_request + if result[0] != 1: + _check_auth_error(result[0], result[1]) + Firebase._print("Client connected as Anonymous") - task._headers = Array(task._headers) + [_AUTHORIZATION_HEADER + auth.idtoken] + task._headers = ["Content-Type: application/json", _AUTHORIZATION_HEADER + auth.idtoken] - var http_request : HTTPRequest - for request in _http_request_pool: - if not request.get_meta("requesting"): - http_request = request - break + var http_request : HTTPRequest + for request in _http_request_pool: + if not request.get_meta("requesting"): + http_request = request + break - if not http_request: - http_request = HTTPRequest.new() - _http_request_pool.append(http_request) - add_child(http_request) - http_request.connect("request_completed", self, "_on_pooled_request_completed", [http_request]) + if not http_request: + http_request = HTTPRequest.new() + Utilities.fix_http_request(http_request) + http_request.accept_gzip = false + _http_request_pool.append(http_request) + add_child(http_request) + http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) - http_request.set_meta("requesting", true) - http_request.set_meta("lifetime", 0.0) - http_request.set_meta("task", task) - http_request.request(task._url, task._headers, true, task._method, task._fields) + http_request.set_meta("requesting", true) + http_request.set_meta("lifetime", 0.0) + http_request.set_meta("task", task) + http_request.request(task._url, task._headers, task._method, task._fields) # ------------- func _on_task_finished(data : Dictionary) : - pass + pass func _on_function_executed(result : int, data : Dictionary) : - pass + pass func _on_task_error(code : int, status : int, message : String): - emit_signal("task_error", code, status, message) - Firebase._printerr(message) + task_error.emit(code, status, message) + Firebase._printerr(message) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - auth = auth_result + auth = auth_result func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - auth = auth_result + auth = auth_result -func _on_pooled_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest) -> void: - request.get_meta("task")._on_request_completed(result, response_code, headers, body) - request.set_meta("requesting", false) +func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: + request.get_meta("task")._on_request_completed(result, response_code, headers, body) + request.set_meta("requesting", false) func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: - _set_offline(result != HTTPRequest.RESULT_SUCCESS) - #_connect_check_node.request(_base_url) + _set_offline(result != HTTPRequest.RESULT_SUCCESS) func _on_FirebaseAuth_logout() -> void: - auth = {} + auth = {} func _check_auth_error(code : int, message : String) -> void: - var err : String - match code: - 400: err = "Please, enable Anonymous Sign-in method or Authenticate the Client before issuing a request (best option)" - Firebase._printerr(err) + var err : String + match code: + 400: err = "Please, enable Anonymous Sign-in method or Authenticate the Client before issuing a request (best option)" + Firebase._printerr(err) diff --git a/addons/godot-firebase/functions/functions.gd.uid b/addons/godot-firebase/functions/functions.gd.uid new file mode 100644 index 0000000..386b092 --- /dev/null +++ b/addons/godot-firebase/functions/functions.gd.uid @@ -0,0 +1 @@ +uid://bb66gkdbbhkdw diff --git a/addons/godot-firebase/icon.svg.import b/addons/godot-firebase/icon.svg.import index 19a1c3b..943b056 100644 --- a/addons/godot-firebase/icon.svg.import +++ b/addons/godot-firebase/icon.svg.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/icon.svg-5c4f39d37c9275a3768de73a392fd315.stex" +type="CompressedTexture2D" +uid="uid://2selq12fp4q0" +path="res://.godot/imported/icon.svg-5c4f39d37c9275a3768de73a392fd315.ctex" metadata={ "vram_texture": false } @@ -10,26 +11,27 @@ metadata={ [deps] source_file="res://addons/godot-firebase/icon.svg" -dest_files=[ "res://.import/icon.svg-5c4f39d37c9275a3768de73a392fd315.stex" ] +dest_files=["res://.godot/imported/icon.svg-5c4f39d37c9275a3768de73a392fd315.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false process/normal_map_invert_y=false -stream=false -size_limit=0 -detect_3d=true +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/godot-firebase/plugin.cfg b/addons/godot-firebase/plugin.cfg index 8d60182..b8ffaac 100644 --- a/addons/godot-firebase/plugin.cfg +++ b/addons/godot-firebase/plugin.cfg @@ -1,7 +1,7 @@ [plugin] name="GodotFirebase" -description="Google Firebase SDK written in GDScript for use in Godot Engine projects." -author="Kyle Szklenski" -version="4.7" +description="Google Firebase SDK written in GDScript for use in Godot Engine 4.0 projects." +author="GodotNutsOrg" +version="1.1" script="plugin.gd" diff --git a/addons/godot-firebase/plugin.gd b/addons/godot-firebase/plugin.gd index d21b2e2..de32c1a 100644 --- a/addons/godot-firebase/plugin.gd +++ b/addons/godot-firebase/plugin.gd @@ -1,8 +1,8 @@ -tool +@tool extends EditorPlugin -func _enter_tree() -> void: - add_autoload_singleton("Firebase", "res://addons/godot-firebase/firebase/firebase.tscn") +func _enable_plugin() -> void: + add_autoload_singleton("Firebase", "res://addons/godot-firebase/firebase/firebase.tscn") -func _exit_tree() -> void: - remove_autoload_singleton("Firebase") +func _disable_plugin() -> void: + remove_autoload_singleton("Firebase") diff --git a/addons/godot-firebase/plugin.gd.uid b/addons/godot-firebase/plugin.gd.uid new file mode 100644 index 0000000..fbba51a --- /dev/null +++ b/addons/godot-firebase/plugin.gd.uid @@ -0,0 +1 @@ +uid://b5kbbihb6v380 diff --git a/addons/godot-firebase/queues/queueable_http_request.gd b/addons/godot-firebase/queues/queueable_http_request.gd new file mode 100644 index 0000000..0143a78 --- /dev/null +++ b/addons/godot-firebase/queues/queueable_http_request.gd @@ -0,0 +1,30 @@ +class_name QueueableHTTPRequest +extends HTTPRequest + +signal queue_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) + +var _queue := [] + +# Determine if we need to set Use Threads to true; it can cause collisions with get_http_client_status() due to a thread returning the data _after_ having checked the connection status and result in double-requests. + +func _ready() -> void: + request_completed.connect( + func(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray): + queue_request_completed.emit(result, response_code, headers, body) + + if not _queue.is_empty(): + var req = _queue.pop_front() + self.request(req.url, req.headers, req.method, req.data) + ) + +func request(url : String, headers : PackedStringArray = PackedStringArray(), method := HTTPClient.METHOD_GET, data : String = "") -> Error: + var status = get_http_client_status() + var result = OK + + if status != HTTPClient.STATUS_DISCONNECTED: + _queue.push_back({url=url, headers=headers, method=method, data=data}) + return result + + result = super.request(url, headers, method, data) + + return result diff --git a/addons/godot-firebase/queues/queueable_http_request.gd.uid b/addons/godot-firebase/queues/queueable_http_request.gd.uid new file mode 100644 index 0000000..8c1c71c --- /dev/null +++ b/addons/godot-firebase/queues/queueable_http_request.gd.uid @@ -0,0 +1 @@ +uid://btr45igqa6wpu diff --git a/addons/godot-firebase/queues/queueable_http_request.tscn b/addons/godot-firebase/queues/queueable_http_request.tscn new file mode 100644 index 0000000..d166941 --- /dev/null +++ b/addons/godot-firebase/queues/queueable_http_request.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://ctb4l7plg8kqg"] + +[ext_resource type="Script" path="res://addons/godot-firebase/queues/queueable_http_request.gd" id="1_2rucc"] + +[node name="QueueableHTTPRequest" type="HTTPRequest"] +script = ExtResource("1_2rucc") diff --git a/addons/godot-firebase/remote_config/firebase_remote_config.gd b/addons/godot-firebase/remote_config/firebase_remote_config.gd new file mode 100644 index 0000000..ee3653e --- /dev/null +++ b/addons/godot-firebase/remote_config/firebase_remote_config.gd @@ -0,0 +1,36 @@ +@tool +class_name FirebaseRemoteConfig +extends Node + +const RemoteConfigFunctionId = "getRemoteConfig" + +signal remote_config_received(config) +signal remote_config_error(error) + +var _project_config = {} +var _headers : PackedStringArray = [ +] +var _auth : Dictionary + +func _set_config(config_json : Dictionary) -> void: + _project_config = config_json # This may get confusing, hoping the variable name makes it easier to understand + +func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: + _auth = auth_result + +func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: + _auth = auth_result + +func _on_FirebaseAuth_logout() -> void: + _auth = {} + +func get_remote_config() -> void: + var function_task = Firebase.Functions.execute("getRemoteConfig", HTTPClient.METHOD_GET, {}, {}) as FunctionTask + var result = await function_task.task_finished + Firebase._print("Config request result: " + str(result)) + if result.has("error"): + remote_config_error.emit(result) + return + + var config = RemoteConfig.new(result) + remote_config_received.emit(config) diff --git a/addons/godot-firebase/remote_config/firebase_remote_config.gd.uid b/addons/godot-firebase/remote_config/firebase_remote_config.gd.uid new file mode 100644 index 0000000..f959ff3 --- /dev/null +++ b/addons/godot-firebase/remote_config/firebase_remote_config.gd.uid @@ -0,0 +1 @@ +uid://d1quoycj5j887 diff --git a/addons/godot-firebase/remote_config/firebase_remote_config.tscn b/addons/godot-firebase/remote_config/firebase_remote_config.tscn new file mode 100644 index 0000000..5c42d3f --- /dev/null +++ b/addons/godot-firebase/remote_config/firebase_remote_config.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3 uid="uid://5xa6ulbllkjk"] + +[ext_resource type="Script" path="res://addons/godot-firebase/remote_config/firebase_remote_config.gd" id="1_wx4ds"] + +[node name="FirebaseRemoteConfig" type="HTTPRequest"] +use_threads = true +script = ExtResource("1_wx4ds") diff --git a/addons/godot-firebase/remote_config/remote_config.gd b/addons/godot-firebase/remote_config/remote_config.gd new file mode 100644 index 0000000..2b72cc6 --- /dev/null +++ b/addons/godot-firebase/remote_config/remote_config.gd @@ -0,0 +1,14 @@ +class_name RemoteConfig +extends RefCounted + +var default_config = {} + +func _init(values : Dictionary) -> void: + default_config = values + +func get_value(key : String) -> Variant: + if default_config.has(key): + return default_config[key] + + Firebase._printerr("Remote config does not contain key: " + key) + return null diff --git a/addons/godot-firebase/remote_config/remote_config.gd.uid b/addons/godot-firebase/remote_config/remote_config.gd.uid new file mode 100644 index 0000000..b1aba17 --- /dev/null +++ b/addons/godot-firebase/remote_config/remote_config.gd.uid @@ -0,0 +1 @@ +uid://dgia6trn0k8sh diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 56da0ea..28d016e 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -4,18 +4,13 @@ ## This object handles all firebase storage tasks, variables and references. To use this API, you must first create a [StorageReference] with [method ref]. With the reference, you can then query and manipulate the file or folder in the cloud storage. ## ## [i]Note: In HTML builds, you must configure [url=https://firebase.google.com/docs/storage/web/download-files#cors_configuration]CORS[/url] in your storage bucket.[i] -tool +@tool class_name FirebaseStorage extends Node const _API_VERSION : String = "v0" -## @arg-types int, int, PoolStringArray -## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode -## Emitted when a [StorageTask] has finished successful. -signal task_successful(result, response_code, data) - -## @arg-types int, int, PoolStringArray +## @arg-types int, int, PackedStringArray ## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode ## Emitted when a [StorageTask] has finished with an error. signal task_failed(result, response_code, data) @@ -41,313 +36,319 @@ var _pending_tasks : Array = [] var _current_task : StorageTask var _response_code : int -var _response_headers : PoolStringArray -var _response_data : PoolByteArray +var _response_headers : PackedStringArray +var _response_data : PackedByteArray var _content_length : int var _reading_body : bool func _notification(what : int) -> void: - if what == NOTIFICATION_INTERNAL_PROCESS: - _internal_process(get_process_delta_time()) + if what == NOTIFICATION_INTERNAL_PROCESS: + _internal_process(get_process_delta_time()) func _internal_process(_delta : float) -> void: - if not requesting: - set_process_internal(false) - return - - var task = _current_task - - match _http_client.get_status(): - HTTPClient.STATUS_DISCONNECTED: - _http_client.connect_to_host(_base_url, 443, true) - - HTTPClient.STATUS_RESOLVING, \ - HTTPClient.STATUS_REQUESTING, \ - HTTPClient.STATUS_CONNECTING: - _http_client.poll() - - HTTPClient.STATUS_CONNECTED: - var err := _http_client.request_raw(task._method, task._url, task._headers, task.data) - if err: - _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) - - HTTPClient.STATUS_BODY: - if _http_client.has_response() or _reading_body: - _reading_body = true - - # If there is a response... - if _response_headers.empty(): - _response_headers = _http_client.get_response_headers() # Get response headers. - _response_code = _http_client.get_response_code() - - for header in _response_headers: - if "Content-Length" in header: - _content_length = header.trim_prefix("Content-Length: ").to_int() - - _http_client.poll() - var chunk = _http_client.read_response_body_chunk() # Get a chunk. - if chunk.size() == 0: - # Got nothing, wait for buffers to fill a bit. - pass - else: - _response_data += chunk # Append to read buffer. - if _content_length != 0: - task.progress = float(_response_data.size()) / _content_length - - if _http_client.get_status() != HTTPClient.STATUS_BODY: - task.progress = 1.0 - _finish_request(HTTPRequest.RESULT_SUCCESS) - else: - task.progress = 1.0 - _finish_request(HTTPRequest.RESULT_SUCCESS) - - HTTPClient.STATUS_CANT_CONNECT: - _finish_request(HTTPRequest.RESULT_CANT_CONNECT) - HTTPClient.STATUS_CANT_RESOLVE: - _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) - HTTPClient.STATUS_CONNECTION_ERROR: - _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) - HTTPClient.STATUS_SSL_HANDSHAKE_ERROR: - _finish_request(HTTPRequest.RESULT_SSL_HANDSHAKE_ERROR) + if not requesting: + set_process_internal(false) + return + + var task = _current_task + + match _http_client.get_status(): + HTTPClient.STATUS_DISCONNECTED: + _http_client.connect_to_host(_base_url, 443, TLSOptions.client()) # Uhh, check if this is going to work. I assume not. + + HTTPClient.STATUS_RESOLVING, \ + HTTPClient.STATUS_REQUESTING, \ + HTTPClient.STATUS_CONNECTING: + _http_client.poll() + + HTTPClient.STATUS_CONNECTED: + var err := _http_client.request_raw(task._method, task._url, task._headers, task.data) + if err: + _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) + + HTTPClient.STATUS_BODY: + if _http_client.has_response() or _reading_body: + _reading_body = true + + # If there is a response... + if _response_headers.is_empty(): + _response_headers = _http_client.get_response_headers() # Get response headers. + _response_code = _http_client.get_response_code() + + for header in _response_headers: + if "Content-Length" in header: + _content_length = header.trim_prefix("Content-Length: ").to_int() + break + + _http_client.poll() + var chunk = _http_client.read_response_body_chunk() # Get a chunk. + if chunk.size() == 0: + # Got nothing, wait for buffers to fill a bit. + pass + else: + _response_data += chunk # Append to read buffer. + if _content_length != 0: + task.progress = float(_response_data.size()) / _content_length + + if _http_client.get_status() != HTTPClient.STATUS_BODY: + task.progress = 1.0 + _finish_request(HTTPRequest.RESULT_SUCCESS) + else: + task.progress = 1.0 + _finish_request(HTTPRequest.RESULT_SUCCESS) + + HTTPClient.STATUS_CANT_CONNECT: + _finish_request(HTTPRequest.RESULT_CANT_CONNECT) + HTTPClient.STATUS_CANT_RESOLVE: + _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) + HTTPClient.STATUS_CONNECTION_ERROR: + _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) + HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: + _finish_request(HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR) ## @args path ## @arg-defaults "" ## @return StorageReference -## Returns a reference to a file or folder in the storage bucket. It's this reference that should be used to control the file/folder on the server end. +## Returns a reference to a file or folder in the storage bucket. It's this reference that should be used to control the file/folder checked the server end. func ref(path := "") -> StorageReference: - if not _config: - return null - - # Create a root storage reference if there's none - # and we're not making one. - if path != "" and not _root_ref: - _root_ref = ref() - - path = _simplify_path(path) - if not _references.has(path): - var ref := StorageReference.new() - _references[path] = ref - ref.valid = true - ref.bucket = bucket - ref.full_path = path - ref.name = path.get_file() - ref.parent = ref(path.plus_file("..")) - ref.root = _root_ref - ref.storage = self - return ref - else: - return _references[path] + if _config == null or _config.is_empty(): + return null + + # Create a root storage reference if there's none + # and we're not making one. + if path != "" and not _root_ref: + _root_ref = ref() + + path = _simplify_path(path) + if not _references.has(path): + var ref := StorageReference.new() + _references[path] = ref + ref.bucket = bucket + ref.full_path = path + ref.file_name = path.get_file() + ref.parent = ref(path.path_join("..")) + ref.root = _root_ref + ref.storage = self + add_child(ref) + return ref + else: + return _references[path] func _set_config(config_json : Dictionary) -> void: - _config = config_json - if bucket != _config.storageBucket: - bucket = _config.storageBucket - _http_client.close() - _check_emulating() + _config = config_json + if bucket != _config.storageBucket: + bucket = _config.storageBucket + _http_client.close() + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://firebasestorage.googleapis.com" - else: - var port : String = _config.emulators.ports.storage - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.") - else: - _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) - - -func _upload(data : PoolByteArray, headers : PoolStringArray, ref : StorageReference, meta_only : bool) -> StorageTask: - if not (_config and _auth): - return null - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) - task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD - task._headers = headers - task.data = data - _process_request(task) - return task - -func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> StorageTask: - if not (_config and _auth): - return null - - var info_task := StorageTask.new() - info_task.ref = ref - info_task._url = _get_file_url(ref) - info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META - _process_request(info_task) - - if url_only or meta_only: - return info_task - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) + "?alt=media&token=" - task.action = StorageTask.Task.TASK_DOWNLOAD - _pending_tasks.append(task) - - yield(info_task, "task_finished") - if info_task.data and not info_task.data.has("error"): - task._url += info_task.data.downloadTokens - else: - task.data = info_task.data - task.response_headers = info_task.response_headers - task.response_code = info_task.response_code - task.result = info_task.result - task.finished = true - task.emit_signal("task_finished") - emit_signal("task_failed", task.result, task.response_code, task.data) - _pending_tasks.erase(task) - - return task - -func _list(ref : StorageReference, list_all : bool) -> StorageTask: - if not (_config and _auth): - return null - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(_root_ref).trim_suffix("/") - task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST - _process_request(task) - return task - -func _delete(ref : StorageReference) -> StorageTask: - if not (_config and _auth): - return null - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) - task.action = StorageTask.Task.TASK_DELETE - _process_request(task) - return task + ## Check emulating + if not Firebase.emulating: + _base_url = "https://firebasestorage.googleapis.com" + else: + var port : String = _config.emulators.ports.storage + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.") + else: + _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) + + +func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> Variant: + if _is_invalid_authentication(): + return 0 + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD + task._headers = headers + task.data = data + _process_request(task) + return await task.task_finished + +func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Variant: + if _is_invalid_authentication(): + return 0 + + var info_task := StorageTask.new() + info_task.ref = ref + info_task._url = _get_file_url(ref) + info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META + _process_request(info_task) + + if url_only or meta_only: + return await info_task.task_finished + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + "?alt=media&token=" + task.action = StorageTask.Task.TASK_DOWNLOAD + _pending_tasks.append(task) + + var data = await info_task.task_finished + if info_task.result == OK: + task._url += info_task.data.downloadTokens + else: + task.data = info_task.data + task.response_headers = info_task.response_headers + task.response_code = info_task.response_code + task.result = info_task.result + task.finished = true + task.task_finished.emit(null) + task_failed.emit(task.result, task.response_code, task.data) + _pending_tasks.erase(task) + return null + + return await task.task_finished + +func _list(ref : StorageReference, list_all : bool) -> Array: + if _is_invalid_authentication(): + return [] + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(_root_ref).trim_suffix("/") + task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST + _process_request(task) + return await task.task_finished + +func _delete(ref : StorageReference) -> bool: + if _is_invalid_authentication(): + return false + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + task.action = StorageTask.Task.TASK_DELETE + _process_request(task) + var data = await task.task_finished + + return data == null func _process_request(task : StorageTask) -> void: - if requesting: - _pending_tasks.append(task) - return - requesting = true - - var headers = Array(task._headers) - headers.append("Authorization: Bearer " + _auth.idtoken) - task._headers = PoolStringArray(headers) - - _current_task = task - _response_code = 0 - _response_headers = PoolStringArray() - _response_data = PoolByteArray() - _content_length = 0 - _reading_body = false - - if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]: - _http_client.close() - set_process_internal(true) + if requesting: + _pending_tasks.append(task) + return + requesting = true + + var headers = Array(task._headers) + headers.append("Authorization: Bearer " + _auth.idtoken) + task._headers = PackedStringArray(headers) + + _current_task = task + _response_code = 0 + _response_headers = PackedStringArray() + _response_data = PackedByteArray() + _content_length = 0 + _reading_body = false + + if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]: + _http_client.close() + set_process_internal(true) func _finish_request(result : int) -> void: - var task := _current_task - requesting = false - - task.result = result - task.response_code = _response_code - task.response_headers = _response_headers - - match task.action: - StorageTask.Task.TASK_DOWNLOAD: - task.data = _response_data - - StorageTask.Task.TASK_DELETE: - _references.erase(task.ref.full_path) - task.ref.valid = false - if typeof(task.data) == TYPE_RAW_ARRAY: - task.data = null - - StorageTask.Task.TASK_DOWNLOAD_URL: - var json : Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result - if json and json.has("downloadTokens"): - task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens - else: - task.data = "" - - StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: - var json : Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result - var items := [] - if json and json.has("items"): - for item in json.items: - var item_name : String = item.name - if item.bucket != bucket: - continue - if not item_name.begins_with(task.ref.full_path): - continue - if task.action == StorageTask.Task.TASK_LIST: - var dir_path : Array = item_name.split("/") - var slash_count : int = task.ref.full_path.count("/") - item_name = "" - for i in slash_count + 1: - item_name += dir_path[i] - if i != slash_count and slash_count != 0: - item_name += "/" - if item_name in items: - continue - - items.append(item_name) - task.data = items - - _: - task.data = JSON.parse(_response_data.get_string_from_utf8()).result - - var next_task : StorageTask - if not _pending_tasks.empty(): - next_task = _pending_tasks.pop_front() - - task.finished = true - task.emit_signal("task_finished") - if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): - emit_signal("task_failed", task.result, task.response_code, task.data) - else: - emit_signal("task_successful", task.result, task.response_code, task.data) - - while true: - if next_task and not next_task.finished: - _process_request(next_task) - break - elif not _pending_tasks.empty(): - next_task = _pending_tasks.pop_front() - else: - break - + var task := _current_task + requesting = false + + task.result = result + task.response_code = _response_code + task.response_headers = _response_headers + + match task.action: + StorageTask.Task.TASK_DOWNLOAD: + task.data = _response_data + + StorageTask.Task.TASK_DELETE: + _references.erase(task.ref.full_path) + for child in get_children(): + if child.full_path == task.ref.full_path: + child.queue_free() + break + if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: + task.data = null + + StorageTask.Task.TASK_DOWNLOAD_URL: + var json = Utilities.get_json_data(_response_data) + if json != null and json.has("downloadTokens"): + task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens + else: + task.data = "" + + StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: + var json = Utilities.get_json_data(_response_data) + var items := [] + if json != null and json.has("items"): + for item in json.items: + var item_name : String = item.name + if item.bucket != bucket: + continue + if not item_name.begins_with(task.ref.full_path): + continue + if task.action == StorageTask.Task.TASK_LIST: + var dir_path : Array = item_name.split("/") + var slash_count : int = task.ref.full_path.count("/") + item_name = "" + for i in slash_count + 1: + item_name += dir_path[i] + if i != slash_count and slash_count != 0: + item_name += "/" + if item_name in items: + continue + + items.append(item_name) + task.data = items + + _: + var json = Utilities.get_json_data(_response_data) + task.data = json + + var next_task = _get_next_pending_task() + + task.finished = true + task.task_finished.emit(task.data) # I believe this parameter has been missing all along, but not sure. Caused weird results at times with a yield/await returning null, but the task containing data. + if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): + task_failed.emit(task.result, task.response_code, task.data) + + if next_task and not next_task.finished: + _process_request(next_task) + +func _get_next_pending_task() -> StorageTask: + if _pending_tasks.is_empty(): + return null + + return _pending_tasks.pop_front() func _get_file_url(ref : StorageReference) -> String: - var url := _extended_url.replace("[APP_ID]", ref.bucket) - url = url.replace("[API_VERSION]", _API_VERSION) - return url.replace("[FILE_PATH]", ref.full_path.replace("/", "%2F")) + var url := _extended_url.replace("[APP_ID]", ref.bucket) + url = url.replace("[API_VERSION]", _API_VERSION) + return url.replace("[FILE_PATH]", ref.full_path.uri_encode()) # Removes any "../" or "./" in the file path. func _simplify_path(path : String) -> String: - var dirs := path.split("/") - var new_dirs := [] - for dir in dirs: - if dir == "..": - new_dirs.pop_back() - elif dir == ".": - pass - else: - new_dirs.push_back(dir) - - var new_path := PoolStringArray(new_dirs).join("/") - new_path = new_path.replace("//", "/") - new_path = new_path.replace("\\", "/") - return new_path + var dirs := path.split("/") + var new_dirs := [] + for dir in dirs: + if dir == "..": + new_dirs.pop_back() + elif dir == ".": + pass + else: + new_dirs.push_back(dir) + + var new_path := "/".join(PackedStringArray(new_dirs)) + new_path = new_path.replace("//", "/") + new_path = new_path.replace("\\", "/") + return new_path func _on_FirebaseAuth_login_succeeded(auth_token : Dictionary) -> void: - _auth = auth_token + _auth = auth_token func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result + _auth = auth_result func _on_FirebaseAuth_logout() -> void: - _auth = {} + _auth = {} + +func _is_invalid_authentication() -> bool: + return (_config == null or _config.is_empty()) or (_auth == null or _auth.is_empty()) diff --git a/addons/godot-firebase/storage/storage.gd.uid b/addons/godot-firebase/storage/storage.gd.uid new file mode 100644 index 0000000..5cb613d --- /dev/null +++ b/addons/godot-firebase/storage/storage.gd.uid @@ -0,0 +1 @@ +uid://pam2nqa7n8u2 diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 56a41f5..2989d12 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -2,42 +2,42 @@ ## @meta-version 2.2 ## A reference to a file or folder in the Firebase cloud storage. ## This object is used to interact with the cloud storage. You may get data from the server, as well as upload your own back to it. -tool +@tool class_name StorageReference -extends Reference +extends Node ## The default MIME type to use when uploading a file. -## Data sent with this type are interpreted as plain binary data. Note that firebase will generate an MIME type based on the file extenstion if none is provided. +## Data sent with this type are interpreted as plain binary data. Note that firebase will generate an MIME type based checked the file extenstion if none is provided. const DEFAULT_MIME_TYPE = "application/octet-stream" -## A dictionary of common MIME types based on a file extension. +## A dictionary of common MIME types based checked a file extension. ## Example: [code]MIME_TYPES.png[/code] will return [code]image/png[/code]. const MIME_TYPES = { - "bmp": "image/bmp", - "css": "text/css", - "csv": "text/csv", - "gd": "text/plain", - "htm": "text/html", - "html": "text/html", - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "json": "application/json", - "mp3": "audio/mpeg", - "mpeg": "video/mpeg", - "ogg": "audio/ogg", - "ogv": "video/ogg", - "png": "image/png", - "shader": "text/plain", - "svg": "image/svg+xml", - "tif": "image/tiff", - "tiff": "image/tiff", - "tres": "text/plain", - "tscn": "text/plain", - "txt": "text/plain", - "wav": "audio/wav", - "webm": "video/webm", - "webp": "video/webm", - "xml": "text/xml", + "bmp": "image/bmp", + "css": "text/css", + "csv": "text/csv", + "gd": "text/plain", + "htm": "text/html", + "html": "text/html", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "json": "application/json", + "mp3": "audio/mpeg", + "mpeg": "video/mpeg", + "ogg": "audio/ogg", + "ogv": "video/ogg", + "png": "image/png", + "shader": "text/plain", + "svg": "image/svg+xml", + "tif": "image/tiff", + "tiff": "image/tiff", + "tres": "text/plain", + "tscn": "text/plain", + "txt": "text/plain", + "wav": "audio/wav", + "webm": "video/webm", + "webp": "image/webp", + "xml": "text/xml", } ## @default "" @@ -51,7 +51,7 @@ var full_path : String = "" ## @default "" ## The name of the file/folder, including any file extension. ## Example: If the [member full_path] is [code]images/user/image.png[/code], then the [member name] would be [code]image.png[/code]. -var name : String = "" +var file_name : String = "" ## The parent [StorageReference] one level up the file hierarchy. ## If the current [StorageReference] is the root (i.e. the [member full_path] is [code]""[/code]) then the [member parent] will be [code]null[/code]. @@ -64,122 +64,96 @@ var root : StorageReference ## The Storage API that created this [StorageReference] to begin with. var storage # FirebaseStorage (Can't static type due to cyclic reference) -## @default false -## Whether this [StorageReference] is valid. None of the functions will work when in an invalid state. -## It is set to false when [method delete] is called. -var valid : bool = false - ## @args path ## @return StorageReference ## Returns a reference to another [StorageReference] relative to this one. func child(path : String) -> StorageReference: - if not valid: - return null - return storage.ref(full_path.plus_file(path)) + return storage.ref(full_path.path_join(path)) ## @args data, metadata -## @return StorageTask -## Makes an attempt to upload data to the referenced file location. Status on this task is found in the returned [StorageTask]. -func put_data(data : PoolByteArray, metadata := {}) -> StorageTask: - if not valid: - return null - if not "Content-Length" in metadata and OS.get_name() != "HTML5": - metadata["Content-Length"] = data.size() +## @return int +## Makes an attempt to upload data to the referenced file location. Returns Variant +func put_data(data : PackedByteArray, metadata := {}) -> Variant: + if not "Content-Length" in metadata and not Utilities.is_web(): + metadata["Content-Length"] = data.size() - var headers := [] - for key in metadata: - headers.append("%s: %s" % [key, metadata[key]]) + var headers := [] + for key in metadata: + headers.append("%s: %s" % [key, metadata[key]]) - return storage._upload(data, headers, self, false) + return await storage._upload(data, headers, self, false) + ## @args data, metadata -## @return StorageTask +## @return int ## Like [method put_data], but [code]data[/code] is a [String]. -func put_string(data : String, metadata := {}) -> StorageTask: - return put_data(data.to_utf8(), metadata) +func put_string(data : String, metadata := {}) -> Variant: + return await put_data(data.to_utf8_buffer(), metadata) ## @args file_path, metadata -## @return StorageTask +## @return int ## Like [method put_data], but the data comes from a file at [code]file_path[/code]. -func put_file(file_path : String, metadata := {}) -> StorageTask: - var file := File.new() - file.open(file_path, File.READ) - var data := file.get_buffer(file.get_len()) - file.close() +func put_file(file_path : String, metadata := {}) -> Variant: + var file := FileAccess.open(file_path, FileAccess.READ) + var data := file.get_buffer(file.get_length()) - if "Content-Type" in metadata: - metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) + if "Content-Type" in metadata: + metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) - return put_data(data, metadata) + return await put_data(data, metadata) -## @return StorageTask -## Makes an attempt to download the files from the referenced file location. Status on this task is found in the returned [StorageTask]. -func get_data() -> StorageTask: - if not valid: - return null - storage._download(self, false, false) - return storage._pending_tasks[-1] +## @return Variant +## Makes an attempt to download the files from the referenced file location. Status checked this task is found in the returned [StorageTask]. +func get_data() -> Variant: + var result = await storage._download(self, false, false) + return result ## @return StorageTask ## Like [method get_data], but the data in the returned [StorageTask] comes in the form of a [String]. -func get_string() -> StorageTask: - var task := get_data() - task.connect("task_finished", self, "_on_task_finished", [task, "stringify"]) - return task +func get_string() -> String: + var task := await get_data() + _on_task_finished(task, "stringify") + return task.data ## @return StorageTask -## Attempts to get the download url that points to the referenced file's data. Using the url directly may require an authentication header. Status on this task is found in the returned [StorageTask]. -func get_download_url() -> StorageTask: - if not valid: - return null - return storage._download(self, false, true) +## Attempts to get the download url that points to the referenced file's data. Using the url directly may require an authentication header. Status checked this task is found in the returned [StorageTask]. +func get_download_url() -> Variant: + return await storage._download(self, false, true) ## @return StorageTask -## Attempts to get the metadata of the referenced file. Status on this task is found in the returned [StorageTask]. -func get_metadata() -> StorageTask: - if not valid: - return null - return storage._download(self, true, false) +## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask]. +func get_metadata() -> Variant: + return await storage._download(self, true, false) ## @args metadata ## @return StorageTask -## Attempts to update the metadata of the referenced file. Any field with a value of [code]null[/code] will be deleted on the server end. Status on this task is found in the returned [StorageTask]. -func update_metadata(metadata : Dictionary) -> StorageTask: - if not valid: - return null - var data := JSON.print(metadata).to_utf8() - var headers := PoolStringArray(["Accept: application/json"]) - return storage._upload(data, headers, self, true) +## Attempts to update the metadata of the referenced file. Any field with a value of [code]null[/code] will be deleted checked the server end. Status checked this task is found in the returned [StorageTask]. +func update_metadata(metadata : Dictionary) -> Variant: + var data := JSON.stringify(metadata).to_utf8_buffer() + var headers := PackedStringArray(["Accept: application/json"]) + return await storage._upload(data, headers, self, true) ## @return StorageTask -## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status on this task is found in the returned [StorageTask]. -func list() -> StorageTask: - if not valid: - return null - return storage._list(self, false) +## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status checked this task is found in the returned [StorageTask]. +func list() -> Array: + return await storage._list(self, false) ## @return StorageTask -## Attempts to get the list of files and/or folders under the referenced folder This function is nested unlike [method list]. Status on this task is found in the returned [StorageTask]. -func list_all() -> StorageTask: - if not valid: - return null - return storage._list(self, true) +## Attempts to get the list of files and/or folders under the referenced folder This function is nested unlike [method list]. Status checked this task is found in the returned [StorageTask]. +func list_all() -> Array: + return await storage._list(self, true) ## @return StorageTask -## Attempts to delete the referenced file/folder. If successful, the reference will become invalid And can no longer be used. If you need to reference this location again, make a new reference with [method StorageTask.ref]. Status on this task is found in the returned [StorageTask]. -func delete() -> StorageTask: - if not valid: - return null - return storage._delete(self) +## Attempts to delete the referenced file/folder. If successful, the reference will become invalid And can no longer be used. If you need to reference this location again, make a new reference with [method StorageTask.ref]. Status checked this task is found in the returned [StorageTask]. +func delete() -> bool: + return await storage._delete(self) func _to_string() -> String: - var string := "gs://%s/%s" % [bucket, full_path] - if not valid: - string += " [Invalid Reference]" - return string + var string := "gs://%s/%s" % [bucket, full_path] + return string func _on_task_finished(task : StorageTask, action : String) -> void: - match action: - "stringify": - if typeof(task.data) == TYPE_RAW_ARRAY: - task.data = task.data.get_string_from_utf8() + match action: + "stringify": + if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: + task.data = task.data.get_string_from_utf8() diff --git a/addons/godot-firebase/storage/storage_reference.gd.uid b/addons/godot-firebase/storage/storage_reference.gd.uid new file mode 100644 index 0000000..821dbc0 --- /dev/null +++ b/addons/godot-firebase/storage/storage_reference.gd.uid @@ -0,0 +1 @@ +uid://d2qowqjuuexg3 diff --git a/addons/godot-firebase/storage/storage_task.gd b/addons/godot-firebase/storage/storage_task.gd index 213b371..fa5cefd 100644 --- a/addons/godot-firebase/storage/storage_task.gd +++ b/addons/godot-firebase/storage/storage_task.gd @@ -1,38 +1,39 @@ -## @meta-authors SIsilicon +## @meta-authors SIsilicon, Kyle 'backat50ft' Szklenski ## @meta-version 2.2 ## An object that keeps track of an operation performed by [StorageReference]. -tool +@tool class_name StorageTask -extends Reference +extends RefCounted enum Task { - TASK_UPLOAD, - TASK_UPLOAD_META, - TASK_DOWNLOAD, - TASK_DOWNLOAD_META, - TASK_DOWNLOAD_URL, - TASK_LIST, - TASK_LIST_ALL, - TASK_DELETE, - TASK_MAX ## The number of [enum Task] constants. + TASK_UPLOAD, + TASK_UPLOAD_META, + TASK_DOWNLOAD, + TASK_DOWNLOAD_META, + TASK_DOWNLOAD_URL, + TASK_LIST, + TASK_LIST_ALL, + TASK_DELETE, + TASK_MAX ## The number of [enum Task] constants. } -## Emitted when the task is finished. Returns data depending on the success and action of the task. +## Emitted when the task is finished. Returns data depending checked the success and action of the task. signal task_finished(data) -## @type StorageReference -## The [StorageReference] that created this [StorageTask]. -var ref # Storage Reference (Can't static type due to cyclic reference) +## Boolean to determine if this request involves metadata only +var is_meta : bool ## @enum Task ## @default -1 ## @setter set_action ## The kind of operation this [StorageTask] is keeping track of. -var action : int = -1 setget set_action +var action : int = -1 : set = set_action -## @default PoolByteArray() +var ref # Should not be needed, damnit + +## @default PackedByteArray() ## Data that the tracked task will/has returned. -var data = PoolByteArray() # data can be of any type. +var data = PackedByteArray() # data can be of any type. ## @default 0.0 ## The percentage of data that has been received. @@ -47,9 +48,9 @@ var result : int = -1 ## Whether the task is finished processing. var finished : bool = false -## @default PoolStringArray() +## @default PackedStringArray() ## The returned HTTP response headers. -var response_headers := PoolStringArray() +var response_headers := PackedStringArray() ## @default 0 ## @enum HTTPClient.ResponseCode @@ -58,16 +59,16 @@ var response_code : int = 0 var _method : int = -1 var _url : String = "" -var _headers : PoolStringArray = PoolStringArray() +var _headers : PackedStringArray = PackedStringArray() func set_action(value : int) -> void: - action = value - match action: - Task.TASK_UPLOAD: - _method = HTTPClient.METHOD_POST - Task.TASK_UPLOAD_META: - _method = HTTPClient.METHOD_PATCH - Task.TASK_DELETE: - _method = HTTPClient.METHOD_DELETE - _: - _method = HTTPClient.METHOD_GET + action = value + match action: + Task.TASK_UPLOAD: + _method = HTTPClient.METHOD_POST + Task.TASK_UPLOAD_META: + _method = HTTPClient.METHOD_PATCH + Task.TASK_DELETE: + _method = HTTPClient.METHOD_DELETE + _: + _method = HTTPClient.METHOD_GET diff --git a/addons/godot-firebase/storage/storage_task.gd.uid b/addons/godot-firebase/storage/storage_task.gd.uid new file mode 100644 index 0000000..fbde286 --- /dev/null +++ b/addons/godot-firebase/storage/storage_task.gd.uid @@ -0,0 +1 @@ +uid://c6glweajcmmv3 diff --git a/addons/http-sse-client/HTTPSSEClient.gd b/addons/http-sse-client/HTTPSSEClient.gd index f15b395..6704c46 100644 --- a/addons/http-sse-client/HTTPSSEClient.gd +++ b/addons/http-sse-client/HTTPSSEClient.gd @@ -1,4 +1,4 @@ -tool +@tool extends Node signal new_sse_event(headers, event, data) @@ -15,111 +15,154 @@ var is_connected = false var domain var url_after_domain var port -var use_ssl -var verify_host +var trusted_chain +var common_name_override var told_to_connect = false var connection_in_progress = false var is_requested = false -var response_body = PoolByteArray() +var response_body = PackedByteArray() -func connect_to_host(domain : String, url_after_domain : String, port : int = -1, use_ssl : bool = false, verify_host : bool = true): - self.domain = domain - self.url_after_domain = url_after_domain - self.port = port - self.use_ssl = use_ssl - self.verify_host = verify_host - told_to_connect = true +func connect_to_host(domain : String, url_after_domain : String, port : int = -1, trusted_chain : X509Certificate = null, common_name_override : String = ""): + process_mode = Node.PROCESS_MODE_INHERIT + self.domain = domain + self.url_after_domain = url_after_domain + self.port = port + self.trusted_chain = trusted_chain + self.common_name_override = common_name_override + told_to_connect = true func attempt_to_connect(): - var err = httpclient.connect_to_host(domain, port, use_ssl, verify_host) - if err == OK: - emit_signal("connected") - is_connected = true - else: - emit_signal("connection_error", str(err)) + var tls_options = TLSOptions.client(trusted_chain, common_name_override) + var err = httpclient.connect_to_host(domain, port, tls_options) + if err == OK: + connected.emit() + is_connected = true + else: + connection_error.emit(str(err)) func attempt_to_request(httpclient_status): - if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING: - return - - if httpclient_status == HTTPClient.STATUS_CONNECTED: - var err = httpclient.request(HTTPClient.METHOD_POST, url_after_domain, ["Accept: text/event-stream"]) - if err == OK: - is_requested = true - -func _parse_response_body(headers): - var body = response_body.get_string_from_utf8() - if body: - var event_data = get_event_data(body) - if event_data.event != "keep-alive" and event_data.event != continue_internal: - var result = JSON.parse(event_data.data).result - if response_body.size() > 0 and result: # stop here if the value doesn't parse - response_body.resize(0) - emit_signal("new_sse_event", headers, event_data.event, result) - else: - if event_data.event != continue_internal: - response_body.resize(0) + if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING: + return + + if httpclient_status == HTTPClient.STATUS_CONNECTED: + var err = httpclient.request(HTTPClient.METHOD_POST, url_after_domain, ["Accept: text/event-stream"]) + if err == OK: + is_requested = true + +#func _parse_response_body(headers): + #var body = response_body.get_string_from_utf8() + #if body: + #var event_data = get_event_data(body) + #if event_data.event != "keep-alive" and event_data.event != continue_internal: + #var result = Utilities.get_json_data(event_data.data) + #if result != null: + #var parsed_text = result + #if response_body.size() > 0: # stop here if the value doesn't parse + #response_body.resize(0) + #new_sse_event.emit(headers, event_data.event, result) + #else: + #if event_data.event != continue_internal: + #response_body.resize(0) func _process(delta): - if !told_to_connect: - return - - if !is_connected: - if !connection_in_progress: - attempt_to_connect() - connection_in_progress = true - return - - httpclient.poll() - var httpclient_status = httpclient.get_status() - if !is_requested: - attempt_to_request(httpclient_status) - return - - if httpclient.has_response() or httpclient_status == HTTPClient.STATUS_BODY: - var headers = httpclient.get_response_headers_as_dictionary() - - if httpclient_status == HTTPClient.STATUS_BODY: - httpclient.poll() - var chunk = httpclient.read_response_body_chunk() - if(chunk.size() == 0): - return - else: - response_body = response_body + chunk - - _parse_response_body(headers) - - elif Firebase.emulating and Firebase._config.workarounds.database_connection_closed_issue: - # Emulation does not send the close connection header currently, so we need to manually read the response body - # see issue https://github.com/firebase/firebase-tools/issues/3329 in firebase-tools - # also comment https://github.com/GodotNuts/GodotFirebase/issues/154#issuecomment-831377763 which explains the issue - while httpclient.connection.get_available_bytes(): - var data = httpclient.connection.get_partial_data(1) - if data[0] == OK: - response_body.append_array(data[1]) - if response_body.size() > 0: - _parse_response_body(headers) - -func get_event_data(body : String) -> Dictionary: - var result = {} - var event_idx = body.find(event_tag) - if event_idx == -1: - result["event"] = continue_internal - return result - assert(event_idx != -1) - var data_idx = body.find(data_tag, event_idx + event_tag.length()) - assert(data_idx != -1) - var event = body.substr(event_idx, data_idx) - event = event.replace(event_tag, "").strip_edges() - assert(event) - assert(event.length() > 0) - result["event"] = event - var data = body.right(data_idx + data_tag.length()).strip_edges() - assert(data) - assert(data.length() > 0) - result["data"] = data - return result + if !told_to_connect: + return + + if !is_connected: + if !connection_in_progress: + attempt_to_connect() + connection_in_progress = true + return + + httpclient.poll() + var httpclient_status = httpclient.get_status() + if !is_requested: + attempt_to_request(httpclient_status) + return + + if httpclient.has_response() or httpclient_status == HTTPClient.STATUS_BODY: + var headers = httpclient.get_response_headers_as_dictionary() + + if httpclient_status == HTTPClient.STATUS_BODY: + httpclient.poll() + var chunk = httpclient.read_response_body_chunk() + if(chunk.size() == 0): + return + else: + response_body = response_body + chunk + + _parse_response_body(headers) + + elif Firebase.emulating and Firebase._config.workarounds.database_connection_closed_issue: + # Emulation does not send the close connection header currently, so we need to manually read the response body + # see issue https://github.com/firebase/firebase-tools/issues/3329 in firebase-tools + # also comment https://github.com/GodotNuts/GodotFirebase/issues/154#issuecomment-831377763 which explains the issue + while httpclient.connection.get_available_bytes(): + var data = httpclient.connection.get_partial_data(1) + if data[0] == OK: + response_body.append_array(data[1]) + if response_body.size() > 0: + _parse_response_body(headers) + +func _parse_response_body(headers): + var body = response_body.get_string_from_utf8() + if body: + var event_datas = get_event_data(body) + var resize_response_body_to_zero_after_for_loop_flag = false + for event_data in event_datas: + if event_data.event != "keep-alive" and event_data.event != continue_internal: + var result = Utilities.get_json_data(event_data.data) + if result != null: + var parsed_text = result + if response_body.size() > 0: + resize_response_body_to_zero_after_for_loop_flag = true + new_sse_event.emit(headers, event_data.event, result) + else: + if event_data.event != continue_internal: + response_body.resize(0) + if resize_response_body_to_zero_after_for_loop_flag: + response_body.resize(0) + + +func get_event_data(body : String) -> Array: + var results = [] + var start_idx = 0 + + if body.find(event_tag, start_idx) == -1: + return [{"event":continue_internal}] + + while true: + # Find the index of the next event tag + var event_idx = body.find(event_tag, start_idx) + if event_idx == -1: + break # No more events found + + # Find the index of the corresponding data tag + var data_idx = body.find(data_tag, event_idx + event_tag.length()) + if data_idx == -1: + break # No corresponding data found + + # Extract the event + var event_value = body.substr(event_idx + event_tag.length(), data_idx - (event_idx + event_tag.length())).strip_edges() + if event_value == "": + break # No valid event value found + + # Extract the data + var data_end = body.find(event_tag, data_idx) # Assume data ends at the next event tag + if data_end == -1: + data_end = body.length() # If no new event tag, read till the end of the body + + var data_value = body.substr(data_idx + data_tag.length(), data_end - (data_idx + data_tag.length())).strip_edges() + if data_value == "": + break # No valid data found + + # Append the event and data to results + results.append({"event": event_value, "data": data_value}) + # Update the start index for the next iteration + start_idx = data_end # Move past the current data section + + return results func _exit_tree(): - if httpclient: - httpclient.close() + if httpclient: + httpclient.close() diff --git a/addons/http-sse-client/HTTPSSEClient.gd.uid b/addons/http-sse-client/HTTPSSEClient.gd.uid new file mode 100644 index 0000000..464298f --- /dev/null +++ b/addons/http-sse-client/HTTPSSEClient.gd.uid @@ -0,0 +1 @@ +uid://bi48jis3vxlim diff --git a/addons/http-sse-client/httpsseclient_plugin.gd b/addons/http-sse-client/httpsseclient_plugin.gd index bda975a..87303c8 100644 --- a/addons/http-sse-client/httpsseclient_plugin.gd +++ b/addons/http-sse-client/httpsseclient_plugin.gd @@ -1,4 +1,4 @@ -tool +@tool extends EditorPlugin func _enter_tree(): diff --git a/addons/http-sse-client/httpsseclient_plugin.gd.uid b/addons/http-sse-client/httpsseclient_plugin.gd.uid new file mode 100644 index 0000000..9dbd802 --- /dev/null +++ b/addons/http-sse-client/httpsseclient_plugin.gd.uid @@ -0,0 +1 @@ +uid://rnkoeltk4f56 diff --git a/addons/http-sse-client/icon.png.import b/addons/http-sse-client/icon.png.import index 9da3f5b..2987cee 100644 --- a/addons/http-sse-client/icon.png.import +++ b/addons/http-sse-client/icon.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/icon.png-fd61c0c5f1d1b4df37e0abe3d8f80985.stex" +type="CompressedTexture2D" +uid="uid://dyexk436ak68q" +path="res://.godot/imported/icon.png-fd61c0c5f1d1b4df37e0abe3d8f80985.ctex" metadata={ "vram_texture": false } @@ -10,25 +11,24 @@ metadata={ [deps] source_file="res://addons/http-sse-client/icon.png" -dest_files=[ "res://.import/icon.png-fd61c0c5f1d1b4df37e0abe3d8f80985.stex" ] +dest_files=["res://.godot/imported/icon.png-fd61c0c5f1d1b4df37e0abe3d8f80985.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/background.png.import b/assets/background.png.import index ff98453..bdd7526 100644 --- a/assets/background.png.import +++ b/assets/background.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/background.png-1fdba8b6a966ce2c2ffc607f7c096e95.stex" +type="CompressedTexture2D" +uid="uid://dg74q2h440e5k" +path="res://.godot/imported/background.png-1fdba8b6a966ce2c2ffc607f7c096e95.ctex" metadata={ "vram_texture": false } @@ -10,25 +11,24 @@ metadata={ [deps] source_file="res://assets/background.png" -dest_files=[ "res://.import/background.png-1fdba8b6a966ce2c2ffc607f7c096e95.stex" ] +dest_files=["res://.godot/imported/background.png-1fdba8b6a966ce2c2ffc607f7c096e95.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/buttons/normal_button.gd b/assets/buttons/normal_button.gd index f4b5393..28c62f3 100644 --- a/assets/buttons/normal_button.gd +++ b/assets/buttons/normal_button.gd @@ -1,5 +1,5 @@ extends TextureButton -export var label : String = '' +@export var label : String = '' func _ready() -> void: - $label.bbcode_text = "[center]" + label + "[/center]" + $label.text = "[center]" + label + "[/center]" diff --git a/assets/buttons/normal_button.gd.uid b/assets/buttons/normal_button.gd.uid new file mode 100644 index 0000000..00b88a3 --- /dev/null +++ b/assets/buttons/normal_button.gd.uid @@ -0,0 +1 @@ +uid://bjg3j1t8bgcb4 diff --git a/assets/buttons/normal_button.png.import b/assets/buttons/normal_button.png.import index 921c8f3..b2e2c7e 100644 --- a/assets/buttons/normal_button.png.import +++ b/assets/buttons/normal_button.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/normal_button.png-ff8611367b0648c5279d2e5c5e41a090.stex" +type="CompressedTexture2D" +uid="uid://dey7w6ieyntb" +path="res://.godot/imported/normal_button.png-ff8611367b0648c5279d2e5c5e41a090.ctex" metadata={ "vram_texture": false } @@ -10,25 +11,24 @@ metadata={ [deps] source_file="res://assets/buttons/normal_button.png" -dest_files=[ "res://.import/normal_button.png-ff8611367b0648c5279d2e5c5e41a090.stex" ] +dest_files=["res://.godot/imported/normal_button.png-ff8611367b0648c5279d2e5c5e41a090.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/buttons/normal_button.tscn b/assets/buttons/normal_button.tscn index 95df88a..83d60fe 100644 --- a/assets/buttons/normal_button.tscn +++ b/assets/buttons/normal_button.tscn @@ -1,32 +1,25 @@ -[gd_scene load_steps=5 format=2] +[gd_scene load_steps=4 format=3 uid="uid://fnf1abx32uum"] -[ext_resource path="res://assets/buttons/normal_button.gd" type="Script" id=1] -[ext_resource path="res://assets/buttons/normal_button.png" type="Texture" id=2] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="DynamicFont" id=3] -[ext_resource path="res://assets/buttons/normal_button_disabled.png" type="Texture" id=4] +[ext_resource type="Script" path="res://assets/buttons/normal_button.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://dey7w6ieyntb" path="res://assets/buttons/normal_button.png" id="2"] +[ext_resource type="Texture2D" uid="uid://d20ruxpv04p8y" path="res://assets/buttons/normal_button_disabled.png" id="4"] [node name="normal_button" type="TextureButton"] +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -margin_left = 0.353546 -margin_top = -0.353546 -margin_right = -529.646 -margin_bottom = -1235.35 -texture_normal = ExtResource( 2 ) -texture_disabled = ExtResource( 4 ) -script = ExtResource( 1 ) -__meta__ = { -"_edit_use_anchors_": false -} +offset_left = 0.353546 +offset_top = -0.353546 +offset_right = -529.646 +offset_bottom = -1235.35 +texture_normal = ExtResource("2") +texture_disabled = ExtResource("4") +script = ExtResource("1") [node name="label" type="RichTextLabel" parent="."] -margin_left = 10.0 -margin_right = 180.0 -margin_bottom = 45.0 +layout_mode = 0 +offset_left = 10.0 +offset_right = 180.0 +offset_bottom = 45.0 mouse_filter = 2 -custom_fonts/normal_font = ExtResource( 3 ) bbcode_enabled = true -fit_content_height = true -__meta__ = { -"_edit_lock_": true -} diff --git a/assets/buttons/normal_button_disabled.png.import b/assets/buttons/normal_button_disabled.png.import index dfc2b03..dc66815 100644 --- a/assets/buttons/normal_button_disabled.png.import +++ b/assets/buttons/normal_button_disabled.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/normal_button_disabled.png-d6d31970ff5f6f54248bc65c6bbf5e66.stex" +type="CompressedTexture2D" +uid="uid://d20ruxpv04p8y" +path="res://.godot/imported/normal_button_disabled.png-d6d31970ff5f6f54248bc65c6bbf5e66.ctex" metadata={ "vram_texture": false } @@ -10,25 +11,24 @@ metadata={ [deps] source_file="res://assets/buttons/normal_button_disabled.png" -dest_files=[ "res://.import/normal_button_disabled.png-d6d31970ff5f6f54248bc65c6bbf5e66.stex" ] +dest_files=["res://.godot/imported/normal_button_disabled.png-d6d31970ff5f6f54248bc65c6bbf5e66.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/image.png.import b/assets/image.png.import index c5d3247..9301d44 100644 --- a/assets/image.png.import +++ b/assets/image.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/image.png-7a7bd711b5b4054e846c70397f9ba855.stex" +type="CompressedTexture2D" +uid="uid://b85wv711cy51y" +path="res://.godot/imported/image.png-7a7bd711b5b4054e846c70397f9ba855.ctex" metadata={ "vram_texture": false } @@ -10,25 +11,24 @@ metadata={ [deps] source_file="res://assets/image.png" -dest_files=[ "res://.import/image.png-7a7bd711b5b4054e846c70397f9ba855.stex" ] +dest_files=["res://.godot/imported/image.png-7a7bd711b5b4054e846c70397f9ba855.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/default_env.tres b/default_env.tres index 20207a4..1a5570b 100644 --- a/default_env.tres +++ b/default_env.tres @@ -1,6 +1,6 @@ [gd_resource type="Environment" load_steps=2 format=2] -[sub_resource type="ProceduralSky" id=1] +[sub_resource type="Sky" id=1] [resource] background_mode = 2 diff --git a/fonts/PermanentMarker.ttf.import b/fonts/PermanentMarker.ttf.import new file mode 100644 index 0000000..d47330d --- /dev/null +++ b/fonts/PermanentMarker.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://ol80od40y6fi" +path="res://.godot/imported/PermanentMarker.ttf-47b835cbb05fc10de2652f8101aca53c.fontdata" + +[deps] + +source_file="res://fonts/PermanentMarker.ttf" +dest_files=["res://.godot/imported/PermanentMarker.ttf-47b835cbb05fc10de2652f8101aca53c.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/fonts/PermanentMarker_18.tres b/fonts/PermanentMarker_18.tres index 7cf01d9..2d521a8 100644 --- a/fonts/PermanentMarker_18.tres +++ b/fonts/PermanentMarker_18.tres @@ -1,6 +1,6 @@ -[gd_resource type="DynamicFont" load_steps=2 format=2] +[gd_resource type="FontFile" load_steps=2 format=2] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://fonts/PermanentMarker.ttf" type="FontFile" id=1] [resource] size = 18 diff --git a/fonts/PermanentMarker_30.tres b/fonts/PermanentMarker_30.tres index 195ce45..73ad67f 100644 --- a/fonts/PermanentMarker_30.tres +++ b/fonts/PermanentMarker_30.tres @@ -1,6 +1,6 @@ -[gd_resource type="DynamicFont" load_steps=2 format=2] +[gd_resource type="FontFile" load_steps=2 format=2] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://fonts/PermanentMarker.ttf" type="FontFile" id=1] [resource] size = 30 diff --git a/fonts/PermanentMarker_72.tres b/fonts/PermanentMarker_72.tres index d435103..4394c51 100644 --- a/fonts/PermanentMarker_72.tres +++ b/fonts/PermanentMarker_72.tres @@ -1,6 +1,6 @@ -[gd_resource type="DynamicFont" load_steps=2 format=2] +[gd_resource type="FontFile" load_steps=2 format=2] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://fonts/PermanentMarker.ttf" type="FontFile" id=1] [resource] size = 72 diff --git a/icon.png.import b/icon.png.import index 96cbf46..0238c58 100644 --- a/icon.png.import +++ b/icon.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +type="CompressedTexture2D" +uid="uid://dw0ssh85ig8c8" +path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" metadata={ "vram_texture": false } @@ -10,25 +11,24 @@ metadata={ [deps] source_file="res://icon.png" -dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] +dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/main.gd b/main.gd index 8f8615f..ba67a98 100644 --- a/main.gd +++ b/main.gd @@ -1,23 +1,26 @@ -extends Node2D +extends Control func _ready(): pass func _on_auth_tests_pressed(): - get_tree().change_scene("res://tests/auth/auth.tscn") + get_tree().change_scene_to_file("res://tests/auth/auth.tscn") func _on_firestore_tests_pressed(): - get_tree().change_scene("res://tests/firestore/firestore.tscn") + get_tree().change_scene_to_file("res://tests/firestore/firestore.tscn") func _on_rtd_tests_pressed(): - get_tree().change_scene("res://tests/database/database.tscn") + get_tree().change_scene_to_file("res://tests/database/database.tscn") func _on_storage_tests_pressed(): - get_tree().change_scene("res://tests/storage/storage.tscn") + get_tree().change_scene_to_file("res://tests/storage/storage.tscn") func _on_dynamiclinks_tests_pressed(): - get_tree().change_scene("res://tests/links/links.tscn") + get_tree().change_scene_to_file("res://tests/links/links.tscn") + +func _on_remoteconfig_tests_pressed() -> void: + get_tree().change_scene_to_file("res://tests/remote_config/remote_config.tscn") diff --git a/main.gd.uid b/main.gd.uid new file mode 100644 index 0000000..056fd3d --- /dev/null +++ b/main.gd.uid @@ -0,0 +1 @@ +uid://deq1gknlp32w8 diff --git a/main.tscn b/main.tscn index e636d3b..6d86660 100644 --- a/main.tscn +++ b/main.tscn @@ -1,149 +1,125 @@ -[gd_scene load_steps=6 format=2] +[gd_scene load_steps=5 format=3 uid="uid://cisx4upsu40uj"] -[ext_resource path="res://main.gd" type="Script" id=1] -[ext_resource path="res://assets/background.png" type="Texture" id=2] -[ext_resource path="res://icon.png" type="Texture" id=3] -[ext_resource path="res://assets/buttons/normal_button.tscn" type="PackedScene" id=4] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="DynamicFont" id=5] +[ext_resource type="Script" path="res://main.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://dg74q2h440e5k" path="res://assets/background.png" id="2"] +[ext_resource type="Texture2D" uid="uid://dw0ssh85ig8c8" path="res://icon.png" id="3"] +[ext_resource type="PackedScene" uid="uid://fnf1abx32uum" path="res://assets/buttons/normal_button.tscn" id="4"] -[node name="main" type="Node2D"] -script = ExtResource( 1 ) +[node name="main" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 -texture = ExtResource( 2 ) -__meta__ = { -"_edit_use_anchors_": false -} +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("2") [node name="menu" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -margin_right = 1024.0 -margin_bottom = 600.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_horizontal = 3 size_flags_vertical = 3 -__meta__ = { -"_edit_use_anchors_": false -} [node name="top" type="VBoxContainer" parent="menu"] -margin_right = 1024.0 -margin_bottom = 197.0 +layout_mode = 2 size_flags_vertical = 3 alignment = 1 [node name="top1" type="HBoxContainer" parent="menu/top"] -margin_top = 23.0 -margin_right = 1024.0 -margin_bottom = 174.0 +layout_mode = 2 alignment = 1 [node name="icon" type="TextureRect" parent="menu/top/top1"] -margin_left = 436.0 -margin_right = 587.0 -margin_bottom = 151.0 -texture = ExtResource( 3 ) +layout_mode = 2 +texture = ExtResource("3") [node name="mid" type="VBoxContainer" parent="menu"] -margin_top = 201.0 -margin_right = 1024.0 -margin_bottom = 398.0 +layout_mode = 2 size_flags_vertical = 3 [node name="mid1" type="VBoxContainer" parent="menu/mid"] -margin_right = 1024.0 -margin_bottom = 96.0 +layout_mode = 2 size_flags_vertical = 3 alignment = 1 [node name="HBoxContainer" type="HBoxContainer" parent="menu/mid/mid1"] -margin_top = 25.0 -margin_right = 1024.0 -margin_bottom = 70.0 +layout_mode = 2 alignment = 1 -[node name="auth_tests" parent="menu/mid/mid1/HBoxContainer" instance=ExtResource( 4 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -margin_left = 223.0 -margin_top = 0.0 -margin_right = 413.0 -margin_bottom = 45.0 +[node name="auth_tests" parent="menu/mid/mid1/HBoxContainer" instance=ExtResource("4")] +layout_mode = 2 label = "Auth" -[node name="firestore_tests" parent="menu/mid/mid1/HBoxContainer" instance=ExtResource( 4 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -margin_left = 417.0 -margin_top = 0.0 -margin_right = 607.0 -margin_bottom = 45.0 +[node name="firestore_tests" parent="menu/mid/mid1/HBoxContainer" instance=ExtResource("4")] +layout_mode = 2 label = "Firestore" -[node name="dynamiclinks_tests" parent="menu/mid/mid1/HBoxContainer" instance=ExtResource( 4 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -margin_left = 611.0 -margin_top = 0.0 -margin_right = 801.0 -margin_bottom = 45.0 +[node name="dynamiclinks_tests" parent="menu/mid/mid1/HBoxContainer" instance=ExtResource("4")] +layout_mode = 2 label = "Links" [node name="mid2" type="VBoxContainer" parent="menu/mid"] -margin_top = 100.0 -margin_right = 1024.0 -margin_bottom = 197.0 +layout_mode = 2 size_flags_vertical = 3 alignment = 1 [node name="HBoxContainer2" type="HBoxContainer" parent="menu/mid/mid2"] -margin_top = 26.0 -margin_right = 1024.0 -margin_bottom = 71.0 +layout_mode = 2 alignment = 1 -[node name="storage_tests" parent="menu/mid/mid2/HBoxContainer2" instance=ExtResource( 4 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -margin_left = 320.0 -margin_top = 0.0 -margin_right = 510.0 -margin_bottom = 45.0 +[node name="storage_tests" parent="menu/mid/mid2/HBoxContainer2" instance=ExtResource("4")] +layout_mode = 2 label = "Storage" -[node name="rtd_tests" parent="menu/mid/mid2/HBoxContainer2" instance=ExtResource( 4 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -margin_left = 514.0 -margin_top = 0.0 -margin_right = 704.0 -margin_bottom = 45.0 +[node name="rtd_tests" parent="menu/mid/mid2/HBoxContainer2" instance=ExtResource("4")] +layout_mode = 2 label = "Database" +[node name="remoteconfig_tests" parent="menu/mid/mid2/HBoxContainer2" instance=ExtResource("4")] +layout_mode = 2 +label = "RemoteConfig" + +[node name="mid3" type="VBoxContainer" parent="menu/mid"] +layout_mode = 2 +size_flags_vertical = 3 +alignment = 1 + +[node name="HBoxContainer3" type="HBoxContainer" parent="menu/mid/mid3"] +layout_mode = 2 +alignment = 1 + + + [node name="bot" type="VBoxContainer" parent="menu"] -margin_top = 402.0 -margin_right = 1024.0 -margin_bottom = 600.0 +layout_mode = 2 size_flags_vertical = 3 [node name="title" type="Label" parent="."] -margin_left = 40.0 -margin_top = 49.009 -margin_right = 412.0 -margin_bottom = 140.009 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 40.0 +offset_top = 49.009 +offset_right = 412.0 +offset_bottom = 140.009 text = "Godot Firebase Testing Utility" -align = 1 -autowrap = true uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} [connection signal="pressed" from="menu/mid/mid1/HBoxContainer/auth_tests" to="." method="_on_auth_tests_pressed"] [connection signal="pressed" from="menu/mid/mid1/HBoxContainer/firestore_tests" to="." method="_on_firestore_tests_pressed"] [connection signal="pressed" from="menu/mid/mid1/HBoxContainer/dynamiclinks_tests" to="." method="_on_dynamiclinks_tests_pressed"] [connection signal="pressed" from="menu/mid/mid2/HBoxContainer2/storage_tests" to="." method="_on_storage_tests_pressed"] [connection signal="pressed" from="menu/mid/mid2/HBoxContainer2/rtd_tests" to="." method="_on_rtd_tests_pressed"] +[connection signal="pressed" from="menu/mid/mid2/HBoxContainer2/remoteconfig_tests" to="." method="_on_remoteconfig_tests_pressed"] diff --git a/project.godot b/project.godot index 0c92cb0..7ce5453 100644 --- a/project.godot +++ b/project.godot @@ -6,127 +6,27 @@ ; [section] ; section goes between [] ; param=value ; assign values to parameters -config_version=4 - -_global_script_classes=[ { -"base": "HTTPRequest", -"class": "FirebaseAuth", -"language": "GDScript", -"path": "res://addons/godot-firebase/auth/auth.gd" -}, { -"base": "Node", -"class": "FirebaseDatabase", -"language": "GDScript", -"path": "res://addons/godot-firebase/database/database.gd" -}, { -"base": "Node", -"class": "FirebaseDatabaseReference", -"language": "GDScript", -"path": "res://addons/godot-firebase/database/reference.gd" -}, { -"base": "Node", -"class": "FirebaseDatabaseStore", -"language": "GDScript", -"path": "res://addons/godot-firebase/database/database_store.gd" -}, { -"base": "Node", -"class": "FirebaseDynamicLinks", -"language": "GDScript", -"path": "res://addons/godot-firebase/dynamiclinks/dynamiclinks.gd" -}, { -"base": "Node", -"class": "FirebaseFirestore", -"language": "GDScript", -"path": "res://addons/godot-firebase/firestore/firestore.gd" -}, { -"base": "Node", -"class": "FirebaseFunctions", -"language": "GDScript", -"path": "res://addons/godot-firebase/functions/functions.gd" -}, { -"base": "Resource", -"class": "FirebaseResource", -"language": "GDScript", -"path": "res://addons/godot-firebase/database/resource.gd" -}, { -"base": "Node", -"class": "FirebaseStorage", -"language": "GDScript", -"path": "res://addons/godot-firebase/storage/storage.gd" -}, { -"base": "Reference", -"class": "FirebaseUserData", -"language": "GDScript", -"path": "res://addons/godot-firebase/auth/user_data.gd" -}, { -"base": "Reference", -"class": "FirestoreCollection", -"language": "GDScript", -"path": "res://addons/godot-firebase/firestore/firestore_collection.gd" -}, { -"base": "Reference", -"class": "FirestoreDocument", -"language": "GDScript", -"path": "res://addons/godot-firebase/firestore/firestore_document.gd" -}, { -"base": "Reference", -"class": "FirestoreQuery", -"language": "GDScript", -"path": "res://addons/godot-firebase/firestore/firestore_query.gd" -}, { -"base": "Reference", -"class": "FirestoreTask", -"language": "GDScript", -"path": "res://addons/godot-firebase/firestore/firestore_task.gd" -}, { -"base": "Reference", -"class": "FunctionTask", -"language": "GDScript", -"path": "res://addons/godot-firebase/functions/function_task.gd" -}, { -"base": "Reference", -"class": "StorageReference", -"language": "GDScript", -"path": "res://addons/godot-firebase/storage/storage_reference.gd" -}, { -"base": "Reference", -"class": "StorageTask", -"language": "GDScript", -"path": "res://addons/godot-firebase/storage/storage_task.gd" -} ] -_global_script_class_icons={ -"FirebaseAuth": "", -"FirebaseDatabase": "", -"FirebaseDatabaseReference": "", -"FirebaseDatabaseStore": "", -"FirebaseDynamicLinks": "", -"FirebaseFirestore": "", -"FirebaseFunctions": "", -"FirebaseResource": "", -"FirebaseStorage": "", -"FirebaseUserData": "", -"FirestoreCollection": "", -"FirestoreDocument": "", -"FirestoreQuery": "", -"FirestoreTask": "", -"FunctionTask": "", -"StorageReference": "", -"StorageTask": "" -} +config_version=5 [application] config/name="FirebaseTestHarness" run/main_scene="res://main.tscn" +config/features=PackedStringArray("4.5") config/icon="res://icon.png" [autoload] Firebase="*res://addons/godot-firebase/firebase/firebase.tscn" +[display] + +window/stretch/mode="canvas_items" +window/dpi/allow_hidpi=false + [editor_plugins] -enabled=PoolStringArray( "res://addons/godot-firebase/plugin.cfg", "res://addons/http-sse-client/plugin.cfg" ) +enabled=PackedStringArray("res://addons/godot-firebase/plugin.cfg", "res://addons/http-sse-client/plugin.cfg") [physics] @@ -134,4 +34,7 @@ common/enable_pause_aware_picking=true [rendering] +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" +textures/vram_compression/import_etc2_astc=true environment/default_environment="res://default_env.tres" diff --git a/tests/auth/auth.gd b/tests/auth/auth.gd index 120bbc6..fddb029 100644 --- a/tests/auth/auth.gd +++ b/tests/auth/auth.gd @@ -1,12 +1,14 @@ -extends Node2D +extends Control # Script used for testing the Authentication functions of the plugin # Variables -onready var _test_running = false -onready var console = $console +@onready var _test_running = false +@onready var console = $console var _auth_error = false +signal test_finished() + # Constants const _email1 = 'test@fakeemail.com' const _email2 = 'test2@fakeemail.com' @@ -16,10 +18,10 @@ const _timer_length = 5 # Function called when the scene is ready func _ready(): - Firebase.Auth.connect("login_succeeded", self, "_on_FirebaseAuth_login_succeeded") - Firebase.Auth.connect("signup_succeeded", self, "_on_FirebaseAuth_signup_succeeded") - Firebase.Auth.connect("login_failed", self, "_on_login_failed") - Firebase.Auth.connect("userdata_received", self, "_on_userdata_received") + Firebase.Auth.login_succeeded.connect(_on_FirebaseAuth_login_succeeded) + Firebase.Auth.signup_succeeded.connect(_on_FirebaseAuth_signup_succeeded) + Firebase.Auth.login_failed.connect(_on_login_failed) + Firebase.Auth.userdata_received.connect(_on_userdata_received) # Function called when the test starts # Clears all checkboxes to clean the GUI @@ -28,7 +30,7 @@ func _test_started() -> void: _test_running = true var checkboxes = get_tree().get_nodes_in_group('tests') for box in checkboxes: - box.pressed = false + box.button_pressed = false $back.disabled = true $test_auth.disabled = true @@ -46,96 +48,94 @@ func _on_test_auth_pressed() -> void: _print_to_console("STARTING AUTH TESTS") _test_started() - # Start signup test using the fist email + # Start signup test using the first email if _auth_error != true: _print_to_console("\nTrying to signup...") Firebase.Auth.signup_with_email_and_password(_email1, _password1) - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout # Start Login test using the first email if _auth_error != true: - $signup_check.pressed = true + $signup_check.button_pressed = true _print_to_console("\nTrying to login...") Firebase.Auth.login_with_email_and_password(_email1, _password1) - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout # Check Auth File if _auth_error != true: - $login_check.pressed = true + $login_check.button_pressed = true _check_auth_file() - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout # Get User Data Test if _auth_error != true: - $auth_file_check.pressed = true + $auth_file_check.button_pressed = true _print_to_console("\nTrying to get user data...") Firebase.Auth.get_user_data() - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout # Change User Password if _auth_error != true: - $user_data_check.pressed = true + $user_data_check.button_pressed = true _print_to_console("\nTrying to change user password...") Firebase.Auth.change_user_password(_password2) - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout Firebase.Auth.logout() - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout Firebase.Auth.login_with_email_and_password(_email1, _password2) - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout # Change the user email from the first email to the second email if _auth_error != true: - $change_pass_check.pressed = true + $change_pass_check.button_pressed = true _print_to_console("\nTrying to change user email...") Firebase.Auth.change_user_email(_email2) - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout Firebase.Auth.logout() - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout Firebase.Auth.login_with_email_and_password(_email2, _password2) - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout # Login with the new credentials if _auth_error != true: - $change_email_check.pressed = true + $change_email_check.button_pressed = true _print_to_console("\nTrying to login with new creds...") Firebase.Auth.login_with_email_and_password(_email2, _password2) - yield(get_tree().create_timer(_timer_length), "timeout") + await get_tree().create_timer(_timer_length).timeout # Start Delete Account test if _auth_error != true: - $login_check_2.pressed = true + $login_check_2.button_pressed = true _print_to_console("\nDeleting Account...") Firebase.Auth.delete_user_account() # If nothing has failed to this point, finish the test successfully if _auth_error != true: - $delete_check.pressed = true + $delete_check.button_pressed = true _print_to_console("\nFINISHED AUTH TESTS") _test_finished() else: _auth_test_error() func _on_FirebaseAuth_signup_succeeded(_auth) -> void: - _print_to_console("Signup with email and password has worked") + _print_to_console("Signup has worked") _print_to_console("Logging Out...") _print_to_console("Ignore this error if there is one, it's normal") Firebase.Auth.logout() _print_to_console("Checking for auth file (It should not be there)...") - var dir = Directory.new() - if (dir.file_exists("user://user.auth")): + if (FileAccess.file_exists("user://user.auth")): _print_to_console_error("Auth file exists, This is bad") _auth_error = true else: _print_to_console("No encrypted auth file exists, good to go") func _on_FirebaseAuth_login_succeeded(_auth) -> void: - _print_to_console("Login with email and password has worked") + _print_to_console("Login has worked") Firebase.Auth.save_auth(_auth) func _check_auth_file() -> void: _print_to_console("Checking for auth file (It should be there)...") - var dir = Directory.new() - if (dir.file_exists("user://user.auth")): + if (FileAccess.file_exists("user://user.auth")): _print_to_console("Auth file exists, good to go") else: _print_to_console_error("No encrypted auth file exists") @@ -143,7 +143,7 @@ func _check_auth_file() -> void: func _on_userdata_received(userdata) -> void: if userdata != null: - _print_to_console("User data recieved") + _print_to_console("User data received") else: _print_to_console_error("Could not get user data") _auth_error = true @@ -155,24 +155,24 @@ func _on_login_failed(error_code, message): func _auth_test_error() -> void: _print_to_console_error("There has been a failure") - emit_signal("test_finished") + test_finished.emit() # Function used to print data to the console GUI for the end user func _print_to_console(data): data = str(data) print(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + data + "\n" - console.bbcode_text = updated_data + console.text = updated_data # Function used to print error data to the console GUI for the end user func _print_to_console_error(data): data = str(data) printerr(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_data + console.text = updated_data # Function called when the end user presses the 'Back' button, returns to the Main Menu func _on_back_pressed(): - get_tree().change_scene("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/auth/auth.gd.uid b/tests/auth/auth.gd.uid new file mode 100644 index 0000000..b22baf5 --- /dev/null +++ b/tests/auth/auth.gd.uid @@ -0,0 +1 @@ +uid://0dpj6u1r8hls diff --git a/tests/auth/auth.tscn b/tests/auth/auth.tscn index 77152b1..980319c 100644 --- a/tests/auth/auth.tscn +++ b/tests/auth/auth.tscn @@ -1,233 +1,193 @@ -[gd_scene load_steps=6 format=2] - -[ext_resource path="res://tests/auth/auth.gd" type="Script" id=1] -[ext_resource path="res://assets/buttons/normal_button.tscn" type="PackedScene" id=2] -[ext_resource path="res://assets/background.png" type="Texture" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="DynamicFont" id=4] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="DynamicFont" id=5] - - -[node name="auth" type="Node2D"] -script = ExtResource( 1 ) +[gd_scene load_steps=4 format=3 uid="uid://cxmrssy30x0qo"] + +[ext_resource type="Script" path="res://tests/auth/auth.gd" id="1"] +[ext_resource type="PackedScene" uid="uid://fnf1abx32uum" path="res://assets/buttons/normal_button.tscn" id="2"] +[ext_resource type="Texture2D" uid="uid://dg74q2h440e5k" path="res://assets/background.png" id="3"] + +[node name="auth" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 -texture = ExtResource( 3 ) -__meta__ = { -"_edit_use_anchors_": false -} +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("3") [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 13.0 -margin_right = 498.0 -margin_bottom = 52.0 -custom_fonts/font = ExtResource( 4 ) +offset_left = 376.0 +offset_top = 13.0 +offset_right = 498.0 +offset_bottom = 52.0 text = "Auth Tests" -align = 1 +horizontal_alignment = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 2 )] -margin_right = 190.354 -margin_bottom = 44.6465 +[node name="back" parent="." instance=ExtResource("2")] +layout_mode = 1 +offset_right = 190.354 +offset_bottom = 44.6465 label = "Back" -[node name="test_auth" parent="." instance=ExtResource( 2 )] -margin_left = 0.0 -margin_top = 65.0 -margin_right = 190.0 -margin_bottom = 110.0 +[node name="test_auth" parent="." instance=ExtResource("2")] +layout_mode = 1 +offset_left = 0.0 +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 label = "Test Auth" [node name="console" type="RichTextLabel" parent="."] -margin_left = 250.586 -margin_top = 117.012 -margin_right = 946.586 -margin_bottom = 522.012 +offset_left = 250.586 +offset_top = 117.012 +offset_right = 946.586 +offset_bottom = 522.012 bbcode_enabled = true scroll_following = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="signup_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 131.0 -margin_right = 34.0 -margin_bottom = 155.0 + +[node name="signup_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="signup_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Signup" -[node name="login_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 155.0 -margin_right = 34.0 -margin_bottom = 179.0 +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Login" -[node name="auth_file_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 179.0 -margin_right = 34.0 -margin_bottom = 203.0 +[node name="auth_file_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 179.0 +offset_right = 34.0 +offset_bottom = 203.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="auth_file_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Auth File Check" -[node name="user_data_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 203.0 -margin_right = 34.0 -margin_bottom = 227.0 +[node name="user_data_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 203.0 +offset_right = 34.0 +offset_bottom = 227.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="user_data_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "User Data Check" -[node name="change_pass_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 227.0 -margin_right = 34.0 -margin_bottom = 251.0 +[node name="change_pass_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 227.0 +offset_right = 34.0 +offset_bottom = 251.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="change_pass_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Change Password" -[node name="change_email_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 251.0 -margin_right = 34.0 -margin_bottom = 275.0 +[node name="change_email_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 251.0 +offset_right = 34.0 +offset_bottom = 275.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="change_email_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Change Email" -[node name="login_check_2" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 275.0 -margin_right = 34.0 -margin_bottom = 299.0 +[node name="login_check_2" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 275.0 +offset_right = 34.0 +offset_bottom = 299.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="login_check_2"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Login Again" -[node name="delete_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 299.0 -margin_right = 34.0 -margin_bottom = 323.0 +[node name="delete_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 299.0 +offset_right = 34.0 +offset_bottom = 323.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="delete_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Delete" [connection signal="pressed" from="back" to="." method="_on_back_pressed"] diff --git a/tests/database/database.gd b/tests/database/database.gd index de8bd0b..95266f4 100644 --- a/tests/database/database.gd +++ b/tests/database/database.gd @@ -1,10 +1,15 @@ -extends Node2D +extends Control # Script used for testing the Realtime Database functions of the plugin +signal database_call_completed + +# When the data key from the push is ready +signal data_key_ready + # Variables -onready var _test_running = false -onready var console = $console +@onready var _test_running = false +@onready var console = $console var database_reference : FirebaseDatabaseReference var added_data_key @@ -14,8 +19,8 @@ const _password : String = 'Password1234' # Function called when the scene is ready func _ready(): - Firebase.Auth.connect("login_succeeded", self, "_on_FirebaseAuth_login_succeeded") - Firebase.Auth.connect("login_failed", self, "_on_login_failed") + Firebase.Auth.connect("login_succeeded",Callable(self,"_on_FirebaseAuth_login_succeeded")) + Firebase.Auth.connect("login_failed",Callable(self,"_on_login_failed")) # Function called when the test starts # Clears all checkboxes to clean the GUI @@ -24,7 +29,7 @@ func _test_started() -> void: _test_running = true var checkboxes = get_tree().get_nodes_in_group('tests') for box in checkboxes: - box.pressed = false + box.button_pressed = false $back.disabled = true $test_database.disabled = true @@ -38,7 +43,7 @@ func _test_finished() -> void: # Function called when login to Firebase has completed successfully func _on_FirebaseAuth_login_succeeded(_auth) -> void: _print_to_console("Login with email and password has worked") - $login_check.pressed = true + $login_check.button_pressed = true _test_database() # Function called when login to Firebase has failed @@ -58,27 +63,47 @@ func _test_database(): _print_to_console("STARTING DATABASE TESTS") # Get the database reference that we will be working with - _print_to_console("\nGetting the Databse Reference...") + _print_to_console("\nGetting the Databse RefCounted...") database_reference = Firebase.Database.get_database_reference("FirebaseTester/data", { }) - $get_ref_check.pressed = true + var once_database_reference = Firebase.Database.get_once_database_reference("FirebaseTester/once/data", { }) + $get_ref_check.button_pressed = true # Connect to signals needed for testing _print_to_console("\nConnecting signals for the RTD...") - database_reference.connect("new_data_update", self, "_on_new_data_update") # for new data - database_reference.connect("patch_data_update", self, "_on_patch_data_update") # for patch data - database_reference.connect("push_failed", self, "_on_push_failed") - database_reference.connect("push_successful", self, "_on_push_successful") + database_reference.connect("new_data_update", _on_new_data_update) # for new data + database_reference.connect("patch_data_update", _on_patch_data_update) # for patch data + database_reference.connect("push_failed", _on_push_failed) + database_reference.connect("push_successful", _on_push_successful) + + once_database_reference.connect("push_failed", _on_once_push_failed) + once_database_reference.connect("push_successful", _on_once_push_successful) + once_database_reference.connect("once_failed", _on_once_failed) + once_database_reference.connect("once_successful", _on_once_successful) # Push data to the RTDB _print_to_console("\nTrying to push data to the RTD...") + once_database_reference.push({'user_name':'username', 'message':'Hello world!'}) database_reference.push({'user_name':'username', 'message':'Hello world!'}) - $push_data_check.pressed = true - yield(get_tree().create_timer(3), "timeout") + $push_data_check.button_pressed = true + await data_key_ready - # Update data in the RTDB + # Get data once from the RTDB + _print_to_console("\n\nAttempting a once-off get from the RTD") + once_database_reference.once(added_data_key + "/user_name") + _print_to_console("Once called") + $once_data_check.button_pressed = true + # Update data in the RTDB _print_to_console("\nTrying to update the DB") database_reference.update(added_data_key, {'user_name':'username', 'message':'Hello world123!'}) - $update_data_check.pressed = true + + _print_to_console("\nTrying to update the DB once") + once_database_reference.update(added_data_key, {'user_name':'username', 'message':'Hello world123!'}) + $update_data_check.button_pressed = true + + # Delete data from the RTDB + _print_to_console("\n\nAttempting to delete data from the RTD") + database_reference.delete(added_data_key + "/user_name") + $delete_data_check.button_pressed = true # If nothing has failed to this point, finish the test successfully _print_to_console("\nFINISHED DATABASE TESTS") @@ -86,42 +111,67 @@ func _test_database(): # Function called when new data has been added to the RTDB func _on_new_data_update(new_data : FirebaseResource): + added_data_key = new_data.key + data_key_ready.emit() _print_to_console(new_data) _print_to_console(new_data.key) _print_to_console(new_data.data) - added_data_key = new_data.key # Function called when data is patched in the RTDB func _on_patch_data_update(patch_data : FirebaseResource): + added_data_key = patch_data.key + data_key_ready.emit() _print_to_console(patch_data) _print_to_console(patch_data.key) _print_to_console(patch_data.data) - added_data_key = patch_data.key # Function called when pushing data to the RTDB has failed func _on_push_failed(): _print_to_console_error("Push failed") + database_call_completed.emit() # Function called when pushing data to the RTDB is successful func _on_push_successful(): + database_call_completed.emit() _print_to_console("Push Successful") +# Function called when pushing data to the RTDB has failed + +func _on_once_push_failed(): + _print_to_console_error("Push failed") + database_call_completed.emit() + +# Function called when pushing data to the RTDB is successful +func _on_once_push_successful(): + database_call_completed.emit() + _print_to_console("Push Successful") + +# Function called when getting data from the RTDB has failed +func _on_once_failed(): + _print_to_console_error("Once failed") + database_call_completed.emit() + +# Function called when pushing data to the RTDB is successful +func _on_once_successful(data): + database_call_completed.emit() + _print_to_console("Once Successful:\n") + _print_to_console(data) # Function used to print data to the console GUI for the end user func _print_to_console(data): data = str(data) print(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + data + "\n" - console.bbcode_text = updated_data + console.text = updated_data # Function used to print error data to the console GUI for the end user func _print_to_console_error(data): data = str(data) printerr(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_data + console.text = updated_data # Function called when the end user presses the 'Back' button, returns to the Main Menu func _on_back_pressed(): - get_tree().change_scene("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/database/database.gd.uid b/tests/database/database.gd.uid new file mode 100644 index 0000000..b0b4bef --- /dev/null +++ b/tests/database/database.gd.uid @@ -0,0 +1 @@ +uid://do4ae73pypenv diff --git a/tests/database/database.tscn b/tests/database/database.tscn index 3afde81..4ce4948 100644 --- a/tests/database/database.tscn +++ b/tests/database/database.tscn @@ -1,176 +1,176 @@ -[gd_scene load_steps=11 format=2] - -[ext_resource path="res://tests/database/database.gd" type="Script" id=1] -[ext_resource path="res://assets/buttons/normal_button.png" type="Texture" id=2] -[ext_resource path="res://assets/background.png" type="Texture" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="DynamicFont" id=4] -[ext_resource path="res://assets/buttons/normal_button.tscn" type="PackedScene" id=5] -[ext_resource path="res://assets/buttons/normal_button_disabled.png" type="Texture" id=6] -[ext_resource path="res://assets/buttons/normal_button.gd" type="Script" id=7] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="DynamicFontData" id=8] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="DynamicFont" id=9] - - -[sub_resource type="DynamicFont" id=1] -size = 24 -font_data = ExtResource( 8 ) - -[node name="database" type="Node2D"] -script = ExtResource( 1 ) +[gd_scene load_steps=7 format=3 uid="uid://c34qiepv6pvbq"] + +[ext_resource type="Script" path="res://tests/database/database.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://dey7w6ieyntb" path="res://assets/buttons/normal_button.png" id="2"] +[ext_resource type="Texture2D" uid="uid://dg74q2h440e5k" path="res://assets/background.png" id="3"] +[ext_resource type="PackedScene" uid="uid://fnf1abx32uum" path="res://assets/buttons/normal_button.tscn" id="5"] +[ext_resource type="Texture2D" uid="uid://d20ruxpv04p8y" path="res://assets/buttons/normal_button_disabled.png" id="6"] +[ext_resource type="Script" path="res://assets/buttons/normal_button.gd" id="7"] + +[node name="database" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 -texture = ExtResource( 3 ) -__meta__ = { -"_edit_use_anchors_": false -} +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("3") [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 13.0 -margin_right = 498.0 -margin_bottom = 52.0 -custom_fonts/font = ExtResource( 4 ) +offset_left = 376.0 +offset_top = 13.0 +offset_right = 498.0 +offset_bottom = 52.0 text = "Database Tests" -align = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 5 )] -margin_right = 190.354 -margin_bottom = 44.6465 +[node name="back" parent="." instance=ExtResource("5")] +layout_mode = 1 +offset_right = 190.354 +offset_bottom = 44.6465 label = "Back" [node name="test_database" type="TextureButton" parent="."] +layout_mode = 1 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -margin_top = 65.0 -margin_right = 190.0 -margin_bottom = 110.0 -texture_normal = ExtResource( 2 ) -texture_disabled = ExtResource( 6 ) -script = ExtResource( 7 ) -__meta__ = { -"_edit_use_anchors_": false -} +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 +texture_normal = ExtResource("2") +texture_disabled = ExtResource("6") +script = ExtResource("7") label = "Test Database" [node name="label" type="RichTextLabel" parent="test_database"] -margin_left = 10.0 -margin_right = 180.0 -margin_bottom = 45.0 +layout_mode = 0 +offset_left = 10.0 +offset_right = 180.0 +offset_bottom = 45.0 mouse_filter = 2 -custom_fonts/normal_font = SubResource( 1 ) bbcode_enabled = true -fit_content_height = true -__meta__ = { -"_edit_lock_": true -} [node name="console" type="RichTextLabel" parent="."] -margin_left = 250.586 -margin_top = 117.012 -margin_right = 946.586 -margin_bottom = 522.012 +offset_left = 250.586 +offset_top = 117.012 +offset_right = 946.586 +offset_bottom = 522.012 bbcode_enabled = true scroll_following = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="login_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 131.0 -margin_right = 34.0 -margin_bottom = 155.0 + +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Login" -[node name="get_ref_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 155.0 -margin_right = 34.0 -margin_bottom = 179.0 +[node name="get_ref_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="get_ref_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) -text = "Get RTDB Reference" - -[node name="push_data_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 179.0 -margin_right = 34.0 -margin_bottom = 203.0 +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 +text = "Get RTDB RefCounted" + +[node name="push_data_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 179.0 +offset_right = 34.0 +offset_bottom = 203.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="push_data_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Push Data" -[node name="update_data_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 203.0 -margin_right = 34.0 -margin_bottom = 227.0 +[node name="update_data_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 203.0 +offset_right = 34.0 +offset_bottom = 227.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="update_data_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Update Data" -__meta__ = { -"_edit_use_anchors_": false -} + +[node name="once_data_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 226.0 +offset_right = 34.0 +offset_bottom = 250.0 +mouse_filter = 2 +disabled = true +button_mask = 0 + +[node name="Label" type="Label" parent="once_data_check"] +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 +text = "Get Data +" + +[node name="delete_data_check" type="CheckBox" parent="." groups=["tests"]] +offset_left = 10.0 +offset_top = 247.0 +offset_right = 34.0 +offset_bottom = 271.0 +mouse_filter = 2 +disabled = true +button_mask = 0 + +[node name="Label" type="Label" parent="delete_data_check"] +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 +text = "Delete Data" [connection signal="pressed" from="back" to="." method="_on_back_pressed"] [connection signal="pressed" from="test_database" to="." method="_on_test_database_pressed"] diff --git a/tests/database/test_etag.gd b/tests/database/test_etag.gd new file mode 100644 index 0000000..f294930 --- /dev/null +++ b/tests/database/test_etag.gd @@ -0,0 +1,51 @@ +extends Control + +var _ref: FirebaseOnceDatabaseReference +var _collection_name = "EtagTest" + +func _ready(): + _run_test() + +func _run_test(): + print("Starting ETag Test") + _ref = Firebase.Database.get_once_database_reference(_collection_name) + _ref.once_successful.connect(_on_once_successful) + _ref.once_failed.connect(_on_once_failed) + + var push_ref = Firebase.Database.get_database_reference(_collection_name) + push_ref.push({"test": "data"}) + await push_ref.push_successful + print("Data pushed") + + _ref.once("") + +func _on_once_successful(data): + print("Once successful") + print("Data: ", data) + + if "last_etag" in _ref and _ref.last_etag != "": + print("ETag received: ", _ref.last_etag) + var etag = _ref.last_etag + + print("Attempting update with CORRECT ETag...") + _ref.update("", {"test": "updated_with_etag"}, etag) + await _ref.push_successful + print("Update with correct ETag successful!") + + print("Attempting update with INCORRECT ETag...") + _ref.update("", {"test": "should_fail"}, "invalid_etag") + var failed = await _wait_for_failure() + if failed: + print("Update with incorrect ETag failed as expected!") + else: + print("ERROR: Update with incorrect ETag succeeded unexpectedly!") + + else: + print("ERROR: No ETag received!") + +func _wait_for_failure() -> bool: + var result = await _ref.push_failed + return true + +func _on_once_failed(): + print("Once failed") diff --git a/tests/database/test_etag.gd.uid b/tests/database/test_etag.gd.uid new file mode 100644 index 0000000..c5d0d7e --- /dev/null +++ b/tests/database/test_etag.gd.uid @@ -0,0 +1 @@ +uid://cmccecu4nculw diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index c83f6fd..cd1761b 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -1,21 +1,30 @@ -extends Node2D +extends Control # Script used for testing the Firestore functions of the plugin # Variables -onready var _test_running = false -onready var console = $console +@onready var _test_running = false +@onready var console = $console var _collection : FirestoreCollection var _document : FirestoreDocument +var _listener +# Constants # Constants const _email : String = 'testaccount@godotnuts.test' const _password : String = 'Password1234' +enum TestMode { NONE, STANDARD, LIVE } +var _current_test_mode = TestMode.NONE + +var listener_test_count := 0 + +var DefaultDocument = { 'name': 'Document1', 'points': 20, 'crud': { 'something': 'other thing', 'main': { 'info' : 8 }}, 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 } + # Function called when the scene is ready func _ready(): - Firebase.Auth.connect("login_succeeded", self, "_on_FirebaseAuth_login_succeeded") - Firebase.Auth.connect("login_failed", self, "_on_login_failed") + Firebase.Auth.login_succeeded.connect(_on_FirebaseAuth_login_succeeded) + Firebase.Auth.login_failed.connect(_on_login_failed) # Function called when the test starts # Clears all checkboxes to clean the GUI @@ -24,9 +33,19 @@ func _test_started() -> void: _test_running = true var checkboxes = get_tree().get_nodes_in_group('tests') for box in checkboxes: - box.pressed = false + box.button_pressed = false $back.disabled = true $test_firestore.disabled = true + $back.disabled = true + $test_firestore.disabled = true + if has_node("live_listener_test"): + $live_listener_test.disabled = true + if has_node("live_controls"): + $live_controls.visible = _current_test_mode == TestMode.LIVE + + if _current_test_mode == TestMode.LIVE: + for box in checkboxes: + box.visible = false # Function called when the tests are finsihed # Re-enables all buttons in the GUI @@ -34,6 +53,14 @@ func _test_finished() -> void: _test_running = false $back.disabled = false $test_firestore.disabled = false + if has_node("live_listener_test"): + $live_listener_test.disabled = false + if has_node("live_controls"): + $live_controls.visible = false + + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.visible = true # Function called if there is an error in the test # Prints the error to the GUI console so the end user can see @@ -41,11 +68,22 @@ func _test_error(data) -> void: _print_to_console_error(data) _test_finished() +func _cleanup_previous_run(): + var previous_run = await _collection.get_doc("Document1") + if previous_run != null: + var deleted = await _collection.delete(previous_run) + if deleted: + _print_to_console("Document1 deleted") + # Function called when login to Firebase has completed successfully func _on_FirebaseAuth_login_succeeded(_auth) -> void: _print_to_console("Login with email and password has worked") - $login_check.pressed = true - _test_firestore() + $login_check.button_pressed = true + + if _current_test_mode == TestMode.STANDARD: + await _test_firestore() + elif _current_test_mode == TestMode.LIVE: + await _start_live_listener_test() # Function called when login to Firebase has failed # Ends the test and prints the error to the GUI console @@ -57,9 +95,106 @@ func _on_login_failed(error_code, message): # Function called when the end user presses the 'Test Auth' button # Starts the test func _on_test_firestore_pressed(): + _current_test_mode = TestMode.STANDARD + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) + +func _on_live_listener_test_pressed(): + _current_test_mode = TestMode.LIVE _test_started() Firebase.Auth.login_with_email_and_password(_email, _password) +func _start_live_listener_test() -> void: + _print_to_console("\nSTARTING LIVE LISTENER TEST") + _print_to_console("Connecting to collection 'Firebasetester'...") + _collection = Firebase.Firestore.collection('Firebasetester') + + _print_to_console("Listening to 'LiveSyncDoc'...") + _document = await _collection.get_doc("LiveSyncDoc") + + if _document == null: + _print_to_console("Document 'LiveSyncDoc' not found, creating it...") + _document = await _collection.add("LiveSyncDoc", { + "status": "listening", + "created_at": Time.get_unix_time_from_system(), + "last_updated": Time.get_unix_time_from_system() + }) + + _print_to_console("Listener attached! Waiting for changes indefinitely...") + _print_to_console("Use the buttons on the left to modify the document.") + + _listener = _document.on_snapshot(func(changes): + _print_to_console("\n[LIVE UPDATE] " + Time.get_time_string_from_system()) + if changes.has("lamport_timestamp"): + _print_to_console("Lamport: %d" % changes.lamport_timestamp) + + if changes.has("updated"): + _print_to_console("Updated: %s" % str(changes.updated)) + if changes.has("added"): + _print_to_console("Added: %s" % str(changes.added)) + if changes.has("removed"): + _print_to_console("Removed: %s" % str(changes.removed)) + + _print_to_console("Current State: %s" % str(_document.get_unsafe_document())) + ) + +func _on_add_field_pressed(): + if _document == null: return + var key = "field_" + str(randi() % 1000) + var value = "value_" + str(randi() % 1000) + _print_to_console("Adding field: %s = %s" % [key, value]) + _document.add_or_update_field(key, value) + await _collection.update(_document) + +func _on_update_field_pressed(): + if _document == null: return + var keys = _document.document.keys() + if keys.size() == 0: + _print_to_console("No fields to update!") + return + + # Filter out metadata fields if you want, but for now just pick random + var key = keys[randi() % keys.size()] + var value = "updated_" + str(randi() % 1000) + _print_to_console("Updating field: %s = %s" % [key, value]) + _document.add_or_update_field(key, value) + await _collection.update(_document) + +func _on_delete_field_pressed(): + if _document == null: return + var keys = _document.document.keys() + if keys.size() == 0: + _print_to_console("No fields to delete!") + return + + var key = keys[randi() % keys.size()] + # Protect critical fields if needed, but for test let's allow deleting anything except maybe doc_name/id which aren't in document dict anyway + _print_to_console("Deleting field: %s" % key) + _document.remove_field(key) + await _collection.update(_document) + +func _on_commit_increment_pressed(): + if not _listener: + print("Listener not initialized!") + return + + var document = await _collection.get_doc("LiveSyncDoc", true) + + var transform = IncrementTransform.new(document.doc_name, true, "counter", 1) + + document.add_field_transform(transform) + + print("Committing increment transform...") + var result = await _collection.commit(document) + print("Commit result: %s" % str(result)) + + # Fetch the latest document state from Firestore to get the calculated value + # This will trigger the listener automatically via replace() -> _emit_changes() -> listener.send_change() + # document = await _collection.get_doc("LiveSyncDoc", false) + # var new_value = document.get_value("counter") + # print("New counter value from server: %s" % str(new_value)) + + # Main function that is run when testing Firestore func _test_firestore() -> void: # Print to the console GUI that the test is starting @@ -69,111 +204,204 @@ func _test_firestore() -> void: _print_to_console("\nConnecting to collection 'Firebasetester'") _collection = Firebase.Firestore.collection('Firebasetester') - # Connect to signals needed for testing - _collection.connect("add_document", self, "on_document_add") - _collection.connect("get_document", self, "on_document_get") - _collection.connect("update_document", self, "on_document_update") - _collection.connect("delete_document", self, "on_document_delete") - _collection.connect("error", self, "on_document_error") - + await _cleanup_previous_run() # Add Document1 to Firestore _print_to_console("Trying to add a document") - var add_task : FirestoreTask = _collection.add("Document1", {'name': 'Document1', 'active': 'true'}) - _document = yield(add_task, "add_document") - $add_document.pressed = true - - # Get Document1 (Document that has been added from the previous step) - _print_to_console("Trying to get 'Document1") - _collection.get('Document1') - _document = yield(_collection, "get_document") + _document = await _collection.add("Document1", DefaultDocument) + $add_document.button_pressed = true + + _print_to_console("Printing crud") + var value = _document.get_value('crud') + _print_to_console(value) + + _print_to_console("Printing crud with dictionary getter") + var value2 = _document['crud'] + _print_to_console(value2) + + _print_to_console("Setting crud value with dictionary setter") + _document['crud'] = "new crud" + _print_to_console("New crud: " + _document['crud']) + + ## Get Document1 (Document that has been added from the previous step) + _print_to_console("Trying to get Document1") + _document = await _collection.get_doc('Document1') + if(_document == null): _test_error("Failed to get document") return - else: - $get_document.pressed = true + + _document = await _collection.get_doc('Document1', true) - # Print Document1 to the console GUI - _print_to_console("Trying to print contents of Document1") - _print_to_console(_document) - $print_document.pressed = true - - # Update Document1 - _print_to_console("Trying to update Document1") - var up_task : FirestoreTask = _collection.update("Document1", {'name': 'Document1', 'active': 'true', 'updated' : 'true'}) - _document = yield(up_task, "update_document") - $update_document.pressed = true - - # Get Document1 (With updated that has been added from the previous step) - _print_to_console("Trying to get 'Document1") - _collection.get('Document1') - _document = yield(_collection, "get_document") - $get_document_2.pressed = true - - # Print Document1 to the console GUI - _print_to_console("Trying to print contents of Document1") + if(_document == null): + _test_error("Failed to get document from cache") + return + + # + ## Print Document1 to the console GUI + _print_to_console("Trying to print unsafe contents of Document1") + _print_to_console(_document.get_unsafe_document()) + $print_document.button_pressed = true + + var timestamp_transform = ServerTimestampTransform.new(_document.doc_name, true, "server_timestamp_attempt") + var increment_transform = IncrementTransform.new(_document.doc_name, true, "increment_field", 2) + var decrement_transform = DecrementTransform.new(_document.doc_name, true, "decrement_field", 2) + var max_transform = MaxTransform.new(_document.doc_name, true, "max_field", 10) # Should update + var min_transform = MinTransform.new(_document.doc_name, true, "min_field", -2) # Should update + _document.add_field_transform(timestamp_transform) + _document.add_field_transform(increment_transform) + _document.add_field_transform(decrement_transform) + _document.add_field_transform(max_transform) + _document.add_field_transform(min_transform) + var commit_changes = await _collection.commit(_document) + + _document = await _collection.get_doc('Document1') + + ### Print Document1 to the console GUI + _print_to_console("Trying to print contents of Document1 after transform") _print_to_console(_document) - $print_document_2.pressed = true + $print_document_2.button_pressed = true + + _print_to_console("Attempting to remove item from Document1") + var previous_document_size = _document.keys().size() + _print_to_console("Current document key size: " + str(previous_document_size)) + + var key = "name" + _document.remove_field(key) + + _document = await _collection.update(_document) - # Delete Document1 from Firestore - _print_to_console("Trying to delete Doucment1") - var del_task : FirestoreTask = _collection.delete("Document1") - _document = yield(del_task, "delete_document") - $delete_document.pressed = true + _print_to_console("After update, document key size is: " + str(_document.keys().size())) - # Query Collection + if previous_document_size == _document.keys().size(): + _print_to_console_error("Did not properly delete item from document") + + ## Query Collection _print_to_console("\nRunning Firestore Query") var query : FirestoreQuery = FirestoreQuery.new() query.from("Firebasetester") query.where("points", FirestoreQuery.OPERATOR.GREATER_THAN, 5) query.order_by("points", FirestoreQuery.DIRECTION.DESCENDING) query.limit(10) - var query_task : FirestoreTask = Firebase.Firestore.query(query) - var result = yield(query_task, "task_finished") + var result = await Firebase.Firestore.query(query) _print_to_console(result) - $run_query.pressed = true + ## Agg Query: Count + _print_to_console("\nRunning Firestore Aggregation Queries") + _print_to_console("\nAgg Query: Count") + var agg_query : FirestoreQuery = FirestoreQuery.new() + agg_query.from("Firebasetester") + agg_query.where("points", FirestoreQuery.OPERATOR.NOT_EQUAL, 5) + agg_query.aggregate().count(10) # Should return something like "count": "1" + var agg_result = await Firebase.Firestore.aggregation_query(agg_query) + _print_to_console("Result from Count query: " + str(agg_result)) + + ## AggQuery: Average + _print_to_console("\nAgg Query: Average") + agg_query = FirestoreQuery.new() + agg_query.from("Firebasetester") + agg_query.where("points", FirestoreQuery.OPERATOR.NOT_EQUAL, 5) + agg_query.aggregate().average("points") # Should return "points": "1" I think + agg_result = await Firebase.Firestore.aggregation_query(agg_query) + _print_to_console("Result from Average query: " + str(agg_result)) + + ## AggQuery: sum + _print_to_console("\nAgg Query: Sum") + agg_query = FirestoreQuery.new() + agg_query.from("Firebasetester") + agg_query.where("points", FirestoreQuery.OPERATOR.NOT_EQUAL, 5) + agg_query.aggregate().sum("points") # Should return "points": "1" ... I think. + agg_result = await Firebase.Firestore.aggregation_query(agg_query) + _print_to_console("Result from Sum query: " + str(agg_result)) + $run_query.button_pressed = true + + ## AggQuery: sum and count for testing purposes + _print_to_console("\nAgg Query: Sum and Count") + agg_query = FirestoreQuery.new() + agg_query.from("Firebasetester") + agg_query.where("points", FirestoreQuery.OPERATOR.NOT_EQUAL, 5) + agg_query.aggregate().sum("points").aggregate().count(10) # Should return "points":"1", "counts":"1" ... I think. + agg_result = await Firebase.Firestore.aggregation_query(agg_query) + _print_to_console("Result from Sum/Count query: " + str(agg_result)) + + $run_query.button_pressed = true + + _print_to_console("Running listener tests") + await run_listener_tests() + + await _cleanup_previous_run() # If nothing has failed to this point, finish the test successfully _print_to_console("\nFINISHED FIRESTORE TESTS") _test_finished() -# Function called when a document has been added to Firestore successfully -func on_document_add(document_added : FirestoreDocument) -> void: - _print_to_console("Document added successfully") - -# Function called when a document has been retrived from Firestore successfully -func on_document_get(document_got : FirestoreDocument) -> void: - _print_to_console("Document got successfully") - -# Function called when a document has been updated in Firestore successfully -func on_document_update(document_updated : FirestoreDocument) -> void: - _print_to_console("Document Updated successfully") - -# Function called when a document has been deleted in Firestore successfully -func on_document_delete() -> void: - _print_to_console("Document deleted successfully") +func run_listener_tests() -> void: + _document = await _collection.get_doc("Document1") + await get_tree().create_timer(0.3).timeout + + var listener = _document.on_snapshot( + func(changes): + _print_to_console("=== Listener Change Received ===") + if changes.has("lamport_timestamp"): + _print_to_console("Lamport Timestamp: %d" % changes.lamport_timestamp) + if changes.has("update_time"): + _print_to_console("Update Time: %.3f" % changes.update_time) + if changes.has("added") and changes.added and changes.added.size() > 0: + _print_to_console("Added: %s" % str(changes.added)) + if changes.has("updated") and changes.updated and changes.updated.size() > 0: + _print_to_console("Updated: %s" % str(changes.updated)) + if changes.has("removed") and changes.removed and changes.removed.size() > 0: + _print_to_console("Removed: %s" % str(changes.removed)) + _print_to_console("Document state: %s" % str(_document.get_unsafe_document())) + _print_to_console("================================") + ) -# Function called when a function with Firestore has failed -func on_document_error(code, status, message) -> void: - _print_to_console_error("error code: " + str(code)) - _print_to_console_error("message: " + str(message)) - _test_error("There was an issue") + + const new_doc_name = 'NewDocument' + + _document.add_or_update_field("name", new_doc_name) + print("Changed name locally") + + await get_tree().create_timer(2.0).timeout + _document.add_or_update_field("name", new_doc_name + "2") + print("Changed name locally again") + + await get_tree().create_timer(2.0).timeout + + _document.add_or_update_field("name", "Document1") + _document.remove_field("active") + print("Changed name locally and removed active") + + _document = await _collection.update(_document) + + var deleted = await _collection.delete(_document) + if deleted: + print("Deleted document") + await get_tree().create_timer(1.0).timeout + + listener.stop() + print("Listener stopped") + + _document = await _collection.add("Document1", DefaultDocument) + print("Document re-added") + + _document.add_or_update_field("name", new_doc_name + "2") + _document = await _collection.update(_document) # Function used to print data to the console GUI for the end user func _print_to_console(data): data = str(data) print(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + data + "\n" - console.bbcode_text = updated_data + console.text = updated_data # Function used to print error data to the console GUI for the end user func _print_to_console_error(data): data = str(data) printerr(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_data + console.text = updated_data # Function called when the end user presses the 'Back' button, returns to the Main Menu func _on_back_pressed(): - get_tree().change_scene("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/firestore/firestore.gd.uid b/tests/firestore/firestore.gd.uid new file mode 100644 index 0000000..dff0edc --- /dev/null +++ b/tests/firestore/firestore.gd.uid @@ -0,0 +1 @@ +uid://tbkx6vnens54 diff --git a/tests/firestore/firestore.tscn b/tests/firestore/firestore.tscn index 01775cc..acf4dce 100644 --- a/tests/firestore/firestore.tscn +++ b/tests/firestore/firestore.tscn @@ -1,288 +1,324 @@ -[gd_scene load_steps=11 format=2] +[gd_scene load_steps=7 format=3 uid="uid://by5qpv1a7wc8n"] -[ext_resource path="res://tests/firestore/firestore.gd" type="Script" id=1] -[ext_resource path="res://assets/buttons/normal_button.png" type="Texture" id=2] -[ext_resource path="res://assets/background.png" type="Texture" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="DynamicFont" id=4] -[ext_resource path="res://assets/buttons/normal_button.tscn" type="PackedScene" id=5] -[ext_resource path="res://assets/buttons/normal_button_disabled.png" type="Texture" id=6] -[ext_resource path="res://assets/buttons/normal_button.gd" type="Script" id=7] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="DynamicFontData" id=8] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="DynamicFont" id=9] +[ext_resource type="Script" uid="uid://tbkx6vnens54" path="res://tests/firestore/firestore.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://dey7w6ieyntb" path="res://assets/buttons/normal_button.png" id="2"] +[ext_resource type="Texture2D" uid="uid://dg74q2h440e5k" path="res://assets/background.png" id="3"] +[ext_resource type="PackedScene" uid="uid://fnf1abx32uum" path="res://assets/buttons/normal_button.tscn" id="5"] +[ext_resource type="Texture2D" uid="uid://d20ruxpv04p8y" path="res://assets/buttons/normal_button_disabled.png" id="6"] +[ext_resource type="Script" uid="uid://bjg3j1t8bgcb4" path="res://assets/buttons/normal_button.gd" id="7"] - -[sub_resource type="DynamicFont" id=1] -size = 23 -font_data = ExtResource( 8 ) - -[node name="firestore" type="Node2D"] -script = ExtResource( 1 ) +[node name="firestore" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] -margin_left = 5.59372 -margin_top = 2.00226 -margin_right = 1029.59 -margin_bottom = 602.002 -texture = ExtResource( 3 ) -__meta__ = { -"_edit_use_anchors_": false -} +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("3") [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 11.8108 -margin_right = 557.0 -margin_bottom = 55.8108 -custom_fonts/font = ExtResource( 4 ) +layout_mode = 0 +offset_left = 376.0 +offset_top = 11.8108 +offset_right = 557.0 +offset_bottom = 55.8108 text = "Firestore Tests" -align = 1 +horizontal_alignment = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 5 )] -margin_right = 190.354 -margin_bottom = 44.6465 +[node name="back" parent="." instance=ExtResource("5")] +layout_mode = 1 +offset_right = 190.354 +offset_bottom = 44.6465 label = "Back" [node name="test_firestore" type="TextureButton" parent="."] +layout_mode = 1 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -margin_top = 65.0 -margin_right = 190.0 -margin_bottom = 110.0 -texture_normal = ExtResource( 2 ) -texture_disabled = ExtResource( 6 ) -script = ExtResource( 7 ) -__meta__ = { -"_edit_use_anchors_": false -} +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 +grow_horizontal = 2 +grow_vertical = 2 +texture_normal = ExtResource("2") +texture_disabled = ExtResource("6") +script = ExtResource("7") label = "Test Firestore" [node name="label" type="RichTextLabel" parent="test_firestore"] -margin_left = 10.0 -margin_right = 180.0 -margin_bottom = 45.0 +layout_mode = 0 +offset_left = 10.0 +offset_right = 180.0 +offset_bottom = 45.0 +mouse_filter = 2 +bbcode_enabled = true + +[node name="live_listener_test" type="TextureButton" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 200.0 +offset_top = 65.0 +offset_right = 390.0 +offset_bottom = 110.0 +grow_horizontal = 2 +grow_vertical = 2 +texture_normal = ExtResource("2") +texture_disabled = ExtResource("6") +script = ExtResource("7") +label = "Live Listener" + +[node name="label" type="RichTextLabel" parent="live_listener_test"] +layout_mode = 0 +offset_left = 10.0 +offset_right = 180.0 +offset_bottom = 45.0 mouse_filter = 2 -custom_fonts/normal_font = SubResource( 1 ) bbcode_enabled = true -fit_content_height = true -__meta__ = { -"_edit_lock_": true -} + +[node name="live_controls" type="Control" parent="."] +visible = false +anchors_preset = 0 +offset_left = 10.0 +offset_top = 120.0 +offset_right = 240.0 +offset_bottom = 300.0 + +[node name="add_field" type="Button" parent="live_controls"] +layout_mode = 0 +offset_right = 200.0 +offset_bottom = 30.0 +text = "Add Random Field" + +[node name="update_field" type="Button" parent="live_controls"] +layout_mode = 0 +offset_top = 40.0 +offset_right = 200.0 +offset_bottom = 70.0 +text = "Update Random Field" + +[node name="delete_field" type="Button" parent="live_controls"] +layout_mode = 0 +offset_top = 80.0 +offset_right = 200.0 +offset_bottom = 110.0 +text = "Delete Random Field" + +[node name="commit_increment" type="Button" parent="live_controls"] +layout_mode = 0 +offset_top = 120.0 +offset_right = 200.0 +offset_bottom = 150.0 +text = "Commit Increment" [node name="console" type="RichTextLabel" parent="."] -margin_left = 249.251 -margin_top = 117.012 -margin_right = 945.251 -margin_bottom = 522.012 +layout_mode = 0 +offset_left = 249.251 +offset_top = 117.012 +offset_right = 945.251 +offset_bottom = 522.012 bbcode_enabled = true scroll_following = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="login_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 131.0 -margin_right = 34.0 -margin_bottom = 155.0 + +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Login" -[node name="add_document" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 155.0 -margin_right = 34.0 -margin_bottom = 179.0 +[node name="add_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="add_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Add Document" -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="get_document" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 180.0 -margin_right = 34.0 -margin_bottom = 204.0 + +[node name="get_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 180.0 +offset_right = 34.0 +offset_bottom = 204.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="get_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Get Document" -[node name="print_document" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 203.0 -margin_right = 34.0 -margin_bottom = 227.0 +[node name="print_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 203.0 +offset_right = 34.0 +offset_bottom = 227.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="print_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Print Document" -[node name="update_document" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 224.0 -margin_right = 34.0 -margin_bottom = 248.0 +[node name="update_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 224.0 +offset_right = 34.0 +offset_bottom = 248.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="update_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Update Document" -[node name="get_document_2" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 246.0 -margin_right = 34.0 -margin_bottom = 270.0 +[node name="delete_item_from_document" type="CheckBox" parent="." groups=["tests"]] +auto_translate_mode = 1 +layout_mode = 0 +offset_left = 10.0 +offset_top = 252.0 +offset_right = 34.0 +offset_bottom = 276.0 +mouse_filter = 2 +disabled = true +button_mask = 0 + +[node name="Label" type="Label" parent="delete_item_from_document"] +auto_translate_mode = 1 +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 +text = "Delete from document" + +[node name="get_document_2" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 275.0 +offset_right = 34.0 +offset_bottom = 299.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="get_document_2"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Get Document" -[node name="print_document_2" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 266.0 -margin_right = 34.0 -margin_bottom = 290.0 +[node name="print_document_2" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 295.0 +offset_right = 34.0 +offset_bottom = 319.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="print_document_2"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Print Document" -[node name="delete_document" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 285.0 -margin_right = 34.0 -margin_bottom = 309.0 +[node name="delete_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 314.0 +offset_right = 34.0 +offset_bottom = 338.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="delete_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Delete Document" -[node name="run_query" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 309.0 -margin_right = 34.0 -margin_bottom = 333.0 +[node name="run_query" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 338.0 +offset_right = 34.0 +offset_bottom = 362.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="run_query"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Run Query" [connection signal="pressed" from="back" to="." method="_on_back_pressed"] [connection signal="pressed" from="test_firestore" to="." method="_on_test_firestore_pressed"] +[connection signal="pressed" from="live_listener_test" to="." method="_on_live_listener_test_pressed"] +[connection signal="pressed" from="live_controls/add_field" to="." method="_on_add_field_pressed"] +[connection signal="pressed" from="live_controls/update_field" to="." method="_on_update_field_pressed"] +[connection signal="pressed" from="live_controls/delete_field" to="." method="_on_delete_field_pressed"] +[connection signal="pressed" from="live_controls/commit_increment" to="." method="_on_commit_increment_pressed"] diff --git a/tests/links/links.gd b/tests/links/links.gd index 54d0574..3958c7c 100644 --- a/tests/links/links.gd +++ b/tests/links/links.gd @@ -1,4 +1,4 @@ -extends Node2D +extends Control # Script used for testing the Dynamic Link functions of the plugin @@ -6,8 +6,8 @@ extends Node2D signal link_printed # Variables -onready var _test_running = false -onready var console = $console +@onready var _test_running = false +@onready var console = $console var link_to_test = 'https://google.com' # Constants @@ -15,8 +15,8 @@ const _email : String = 'testaccount@godotnuts.test' const _password : String = 'Password1234' func _ready(): - Firebase.Auth.connect("login_succeeded", self, "_on_FirebaseAuth_login_succeeded") - Firebase.Auth.connect("login_failed", self, "_on_login_failed") + Firebase.Auth.connect("login_succeeded",Callable(self,"_on_FirebaseAuth_login_succeeded")) + Firebase.Auth.connect("login_failed",Callable(self,"_on_login_failed")) # Function called when the test starts # Clears all checkboxes to clean the GUI @@ -25,7 +25,7 @@ func _test_started() -> void: _test_running = true var checkboxes = get_tree().get_nodes_in_group('tests') for box in checkboxes: - box.pressed = false + box.button_pressed = false $back.disabled = true $test_links.disabled = true @@ -39,7 +39,7 @@ func _test_finished() -> void: # Function called when login to Firebase has completed successfully func _on_FirebaseAuth_login_succeeded(_auth) -> void: _print_to_console("Login with email and password has worked") - $login_check.pressed = true + $login_check.button_pressed = true _test_links() # Function called when login to Firebase has failed @@ -67,19 +67,19 @@ func _test_links(): _print_to_console("STARTING LINKS TESTS") # Connect to signals needed for testing - Firebase.DynamicLinks.connect("dynamic_link_generated", self, "print_link") + Firebase.DynamicLinks.connect("dynamic_link_generated",Callable(self,"print_link")) # Generate 'Unguessable Link' _print_to_console("\nTrying to generate an unguessable link...") Firebase.DynamicLinks.generate_dynamic_link(link_to_test, "", "", true) - yield(self, 'link_printed') - $unguessable_link_check.pressed = true + await self.link_printed + $unguessable_link_check.button_pressed = true # Generate 'Guessable Link' _print_to_console("\nTrying to generate an guessable link...") Firebase.DynamicLinks.generate_dynamic_link(link_to_test, "", "", false) - yield(self, 'link_printed') - $guessable_link_check.pressed = true + await self.link_printed + $guessable_link_check.button_pressed = true # If nothing has failed to this point, finish the test successfully _print_to_console("\nFINISHED LINKS TESTS") @@ -94,18 +94,18 @@ func print_link(link_data): func _print_to_console(data): data = str(data) print(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + data + "\n" - console.bbcode_text = updated_data + console.text = updated_data # Function used to print error data to the console GUI for the end user func _print_to_console_error(data): data = str(data) printerr(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_data + console.text = updated_data # Function called when the end user presses the 'Back' button, returns to the Main Menu func _on_back_pressed(): - get_tree().change_scene("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/links/links.gd.uid b/tests/links/links.gd.uid new file mode 100644 index 0000000..6d8baae --- /dev/null +++ b/tests/links/links.gd.uid @@ -0,0 +1 @@ +uid://7a7be1ydmk8y diff --git a/tests/links/links.tscn b/tests/links/links.tscn index c7d6af5..d324a68 100644 --- a/tests/links/links.tscn +++ b/tests/links/links.tscn @@ -1,122 +1,112 @@ -[gd_scene load_steps=6 format=2] +[gd_scene load_steps=4 format=3 uid="uid://xkbp42vcc0wc"] -[ext_resource path="res://tests/links/links.gd" type="Script" id=1] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="DynamicFont" id=2] -[ext_resource path="res://assets/background.png" type="Texture" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="DynamicFont" id=4] -[ext_resource path="res://assets/buttons/normal_button.tscn" type="PackedScene" id=5] +[ext_resource type="Script" path="res://tests/links/links.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://dg74q2h440e5k" path="res://assets/background.png" id="3"] +[ext_resource type="PackedScene" uid="uid://fnf1abx32uum" path="res://assets/buttons/normal_button.tscn" id="5"] - -[node name="links" type="Node2D"] -script = ExtResource( 1 ) +[node name="links" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 -texture = ExtResource( 3 ) -__meta__ = { -"_edit_use_anchors_": false -} +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("3") [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 13.0 -margin_right = 498.0 -margin_bottom = 52.0 -custom_fonts/font = ExtResource( 4 ) +layout_mode = 0 +offset_left = 376.0 +offset_top = 13.0 +offset_right = 498.0 +offset_bottom = 52.0 text = "Links Tests" -align = 1 +horizontal_alignment = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 5 )] -margin_right = 190.354 -margin_bottom = 44.6465 +[node name="back" parent="." instance=ExtResource("5")] +layout_mode = 1 +offset_right = 190.354 +offset_bottom = 44.6465 label = "Back" -[node name="test_links" parent="." instance=ExtResource( 5 )] -margin_top = 65.0 -margin_right = 190.0 -margin_bottom = 110.0 +[node name="test_links" parent="." instance=ExtResource("5")] +layout_mode = 1 +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 label = "Test Links" [node name="console" type="RichTextLabel" parent="."] -margin_left = 250.586 -margin_top = 117.012 -margin_right = 946.586 -margin_bottom = 522.012 +layout_mode = 0 +offset_left = 250.586 +offset_top = 117.012 +offset_right = 946.586 +offset_bottom = 522.012 bbcode_enabled = true scroll_following = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="login_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 131.0 -margin_right = 34.0 -margin_bottom = 155.0 +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 2 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Login" -[node name="unguessable_link_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 155.0 -margin_right = 34.0 -margin_bottom = 179.0 +[node name="unguessable_link_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="unguessable_link_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 2 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Unguessable Link" -[node name="guessable_link_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 179.0 -margin_right = 34.0 -margin_bottom = 203.0 +[node name="guessable_link_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 179.0 +offset_right = 34.0 +offset_bottom = 203.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="guessable_link_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 2 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Guessable Link" [connection signal="pressed" from="back" to="." method="_on_back_pressed"] diff --git a/tests/remote_config/remote_config.gd b/tests/remote_config/remote_config.gd new file mode 100644 index 0000000..e46dac5 --- /dev/null +++ b/tests/remote_config/remote_config.gd @@ -0,0 +1,107 @@ +extends Control + +# Script used for testing the RemoteConfig API of the plugin + +# Variables +@onready var _test_running = false +@onready var console = $console + +# Constants +const _email : String = 'testaccount@godotnuts.test' +const _password : String = 'Password1234' + +# Function called when the scene is ready +func _ready(): + Firebase.Auth.login_succeeded.connect(_on_FirebaseAuth_login_succeeded) + Firebase.Auth.login_failed.connect(_on_login_failed) + +# Function called when the test starts +# Clears all checkboxes to clean the GUI +# Disbales all buttons in the GUI to allow the test to run uninterupted +func _test_started() -> void: + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + %back.disabled = true + %test_remoteconfig.disabled = true + +# Function called when the tests are finsihed +# Re-enables all buttons in the GUI +func _test_finished() -> void: + _test_running = false + %back.disabled = false + %test_remoteconfig.disabled = false + +# Function called when login to Firebase has completed successfully +func _on_FirebaseAuth_login_succeeded(_auth) -> void: + _print_to_console("Login with email and password has worked") + %login_check.button_pressed = true + _test_remote_config() + +# Function called when login to Firebase has failed +# Ends the test and prints the error to the GUI console +func _on_login_failed(error_code, message): + _print_to_console_error("error code: " + str(error_code)) + _print_to_console_error("message: " + str(message)) + +# Function called when the end user presses the 'Test Database' button +# Starts the test +func _on_test_remote_config_pressed(): + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) + +func _test_remote_config(): + # Print to the console GUI that the test is starting + _print_to_console("STARTING REMOTE CONFIG TESTS") + + # Get the RemoteConfig reference that we will be working with + _print_to_console("\nGetting the Remote Config...") + var remote_config = Firebase.RemoteConfigAPI + + # Connect to signals needed for testing + _print_to_console("\nConnecting signals for Remote Config...") + remote_config.remote_config_received.connect(_on_remote_config_received, CONNECT_REFERENCE_COUNTED) + remote_config.remote_config_error.connect(_on_remote_config_error, CONNECT_REFERENCE_COUNTED) + + var reduce_signal = Utilities.SignalReducer.new() + reduce_signal.add_signal(remote_config.remote_config_received, 1) + reduce_signal.add_signal(remote_config.remote_config_error, 1) + + _print_to_console("\nCalling to get Remote Config") + + remote_config.get_remote_config() + + await reduce_signal.completed + # If nothing has failed to this point, finish the test successfully + _print_to_console("\nFINISHED REMOTECONFIG TESTS") + _test_finished() + +func _on_remote_config_received(config) -> void: + _print_to_console("Config received: ") + _print_to_console(config) + %get_remote_config.button_pressed = true + +func _on_remote_config_error(error) -> void: + _print_to_console("Error received: ") + _print_to_console_error(error) + +# Function used to print data to the console GUI for the end user +func _print_to_console(data): + data = str(data) + print(data) + var previous_data = console.text + var updated_data = previous_data + data + "\n" + console.text = updated_data + +# Function used to print error data to the console GUI for the end user +func _print_to_console_error(data): + data = str(data) + printerr(data) + var previous_data = console.text + var updated_data = previous_data + "[color=red]" + data + "[/color] \n" + console.text = updated_data + +# Function called when the end user presses the 'Back' button, returns to the Main Menu +func _on_back_pressed(): + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/remote_config/remote_config.gd.uid b/tests/remote_config/remote_config.gd.uid new file mode 100644 index 0000000..a4186bb --- /dev/null +++ b/tests/remote_config/remote_config.gd.uid @@ -0,0 +1 @@ +uid://cbun7gcobq2ow diff --git a/tests/remote_config/remote_config.tscn b/tests/remote_config/remote_config.tscn new file mode 100644 index 0000000..7e14acd --- /dev/null +++ b/tests/remote_config/remote_config.tscn @@ -0,0 +1,115 @@ +[gd_scene load_steps=7 format=3 uid="uid://dfwguo732qgmx"] + +[ext_resource type="Script" path="res://tests/remote_config/remote_config.gd" id="1_ndtir"] +[ext_resource type="Texture2D" uid="uid://dg74q2h440e5k" path="res://assets/background.png" id="2_245u4"] +[ext_resource type="PackedScene" uid="uid://fnf1abx32uum" path="res://assets/buttons/normal_button.tscn" id="3_qhrqh"] +[ext_resource type="Texture2D" uid="uid://dey7w6ieyntb" path="res://assets/buttons/normal_button.png" id="4_ucqi5"] +[ext_resource type="Texture2D" uid="uid://d20ruxpv04p8y" path="res://assets/buttons/normal_button_disabled.png" id="5_dbun1"] +[ext_resource type="Script" path="res://assets/buttons/normal_button.gd" id="6_2y600"] + +[node name="remote_config" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1_ndtir") + +[node name="background" type="TextureRect" parent="."] +layout_mode = 0 +offset_right = 1024.0 +offset_bottom = 600.0 +texture = ExtResource("2_245u4") + +[node name="title" type="Label" parent="."] +layout_mode = 0 +offset_left = 376.0 +offset_top = 13.0 +offset_right = 498.0 +offset_bottom = 52.0 +text = "Remote Config Tests" +uppercase = true + +[node name="back" parent="." instance=ExtResource("3_qhrqh")] +unique_name_in_owner = true +layout_mode = 1 +offset_right = 190.354 +offset_bottom = 44.6465 +label = "Back" + +[node name="test_remoteconfig" type="TextureButton" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 +grow_horizontal = 2 +grow_vertical = 2 +texture_normal = ExtResource("4_ucqi5") +texture_disabled = ExtResource("5_dbun1") +script = ExtResource("6_2y600") +label = "Test Remote Config" + +[node name="label" type="RichTextLabel" parent="test_remoteconfig"] +layout_mode = 0 +offset_left = 10.0 +offset_right = 180.0 +offset_bottom = 45.0 +mouse_filter = 2 +bbcode_enabled = true + +[node name="console" type="RichTextLabel" parent="."] +layout_mode = 0 +offset_left = 250.586 +offset_top = 117.012 +offset_right = 946.586 +offset_bottom = 522.012 +bbcode_enabled = true +scroll_following = true + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +offset_left = 4.0 +offset_top = 136.0 +offset_right = 28.0 +offset_bottom = 188.0 + +[node name="login_check" type="CheckBox" parent="VBoxContainer" groups=["tests"]] +unique_name_in_owner = true +auto_translate_mode = 1 +layout_mode = 2 +mouse_filter = 2 +disabled = true +button_mask = 0 + +[node name="Label" type="Label" parent="VBoxContainer/login_check"] +auto_translate_mode = 1 +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 +text = "Login" + +[node name="get_remote_config" type="CheckBox" parent="VBoxContainer" groups=["tests"]] +unique_name_in_owner = true +layout_mode = 2 +mouse_filter = 2 +disabled = true +button_mask = 0 + +[node name="Label" type="Label" parent="VBoxContainer/get_remote_config"] +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 +text = "Got Remote Config" + +[connection signal="pressed" from="back" to="." method="_on_back_pressed"] +[connection signal="pressed" from="test_remoteconfig" to="." method="_on_test_remote_config_pressed"] diff --git a/tests/storage/storage.gd b/tests/storage/storage.gd index 96717dd..636ef60 100644 --- a/tests/storage/storage.gd +++ b/tests/storage/storage.gd @@ -1,10 +1,10 @@ -extends Node2D +extends Control # Script used for testing the Storage functions of the plugin # Variables -onready var _test_running = false -onready var console = $console +@onready var _test_running = false +@onready var console = $console # Constants const _email : String = 'testaccount@godotnuts.test' @@ -12,8 +12,8 @@ const _password : String = 'Password1234' # Function called when the scene is ready func _ready(): - Firebase.Auth.connect("login_succeeded", self, "_on_FirebaseAuth_login_succeeded") - Firebase.Auth.connect("login_failed", self, "_on_login_failed") + Firebase.Auth.login_succeeded.connect(_on_FirebaseAuth_login_succeeded) + Firebase.Auth.login_failed.connect(_on_login_failed) # Function called when the test starts # Clears all checkboxes to clean the GUI @@ -22,7 +22,7 @@ func _test_started() -> void: _test_running = true var checkboxes = get_tree().get_nodes_in_group('tests') for box in checkboxes: - box.pressed = false + box.button_pressed = false $back.disabled = true $test_storage.disabled = true $image.texture = null @@ -37,7 +37,7 @@ func _test_finished() -> void: # Function called when login to Firebase has completed successfully func _on_FirebaseAuth_login_succeeded(_auth) -> void: _print_to_console("Login with email and password has worked") - $login_check.pressed = true + $login_check.button_pressed = true _test_storage() # Function called when login to Firebase has failed @@ -59,128 +59,110 @@ func _test_storage(): # Upload test image to Storage _print_to_console("Trying to Upload image...") - var upload_task = Firebase.Storage.ref("Firebasetester/upload/image.png").put_file("res://assets/image.png") - yield(upload_task, "task_finished") - $upload_image_check.pressed = true + var upload = await Firebase.Storage.ref("Firebasetester/upload/image.png").put_file("res://assets/image.png") + $upload_image_check.button_pressed = true # Download image and display it in the GUi for the end user _print_to_console("\nTrying to download image and display it...") - var image = get_image('image.png') - yield(image, "task_finished") - var converted_image = task2image(image) + var image = await get_image('image.png') + var converted_image = variant2image(image) $image.texture = converted_image - $download_image_check.pressed = true + $download_image_check.button_pressed = true # Get download URL for the image and display it in the GUI to the end user _print_to_console("\nTrying to get download URL...") - var url_task = Firebase.Storage.ref("Firebasetester/upload/image.png").get_download_url() - yield(url_task, "task_finished") - _print_to_console(url_task.data) - $image_url_check.pressed = true + var url = await Firebase.Storage.ref("Firebasetester/upload/image.png").get_download_url() + _print_to_console(url) + $image_url_check.button_pressed = true # Get the metadata for the image and display it in the GUI to the end user _print_to_console("\nTrying to get the metadata...") - var meta_task = Firebase.Storage.ref("Firebasetester/upload/image.png").get_metadata() - yield(meta_task, "task_finished") - _print_to_console(meta_task.data) - $image_meta_check.pressed = true + var meta = await Firebase.Storage.ref("Firebasetester/upload/image.png").get_metadata() + _print_to_console(meta) + $image_meta_check.button_pressed = true # Delete the test image from Storage _print_to_console("\nTrying to delete file...") _print_to_console("Before Delete...") - var list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - yield(list_all_task, "task_finished") - _print_to_console(list_all_task.data) - var delete_task = Firebase.Storage.ref("Firebasetester/upload/image.png").delete() - yield(delete_task, "task_finished") + await list_all_current() + var delete = await Firebase.Storage.ref("Firebasetester/upload/image.png").delete() _print_to_console("After Delete...") - list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - yield(list_all_task, "task_finished") - _print_to_console(list_all_task.data) - $image_delete_check.pressed = true + await list_all_current() + $image_delete_check.button_pressed = true # Upload test document to Storage _print_to_console("\nTrying to upload file") - upload_task = Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").put_file("res://assets/dummy.pdf") - yield(upload_task, "task_finished") - $upload_document_check.pressed = true + upload = await Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").put_file("res://assets/dummy.pdf") + $upload_document_check.button_pressed = true # Get the metadata for the document and display it in the GUI to the end user _print_to_console("\nTrying to get the metadata...") - meta_task = Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").get_metadata() - yield(meta_task, "task_finished") - _print_to_console(meta_task.data) - $document_meta_check.pressed = true + meta = await Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").get_metadata() + _print_to_console(meta) + $document_meta_check.button_pressed = true # Delete the test document from Storage _print_to_console("\nTrying to delete file...") _print_to_console("Before Delete...") - list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - yield(list_all_task, "task_finished") - _print_to_console(list_all_task.data) - delete_task = Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").delete() - yield(delete_task, "task_finished") + await list_all_current() + delete = await Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").delete() _print_to_console("After Delete...") - list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - yield(list_all_task, "task_finished") - _print_to_console(list_all_task.data) - $document_delete_check.pressed = true + await list_all_current() + $document_delete_check.button_pressed = true # Upload string to Storage _print_to_console("\nTrying to write a string...") - upload_task = Firebase.Storage.ref("Firebasetester/upload/junkdata").put_string("Test", {}) - yield(upload_task, "task_finished") - $upload_string_check.pressed = true + upload = await Firebase.Storage.ref("Firebasetester/upload/junkdata").put_string("Test", {}) + $upload_string_check.button_pressed = true # Add metadata to the string _print_to_console("\nTrying to add metadata to it...") - meta_task = Firebase.Storage.ref("Firebasetester/upload/junkdata").update_metadata({"Test": "This is a Test", "SillyData": "We got it"}) - yield(meta_task, "task_finished") - $string_add_meta_check.pressed = true + meta = await Firebase.Storage.ref("Firebasetester/upload/junkdata").update_metadata({"Test": "This is a Test", "SillyData": "We got it"}) + $string_add_meta_check.button_pressed = true # Get the metadata for the string and display it in the GUI to the end user _print_to_console("\nTrying to get the metadata...") - meta_task = Firebase.Storage.ref("Firebasetester/upload/junkdata").get_metadata() - yield(meta_task, "task_finished") - _print_to_console(meta_task.data) - $string_meta_check.pressed = true + meta = await Firebase.Storage.ref("Firebasetester/upload/junkdata").get_metadata() + _print_to_console(meta) + $string_meta_check.button_pressed = true # Delete the test string from Storage _print_to_console("\nTrying to delete file...") _print_to_console("Before Delete...") - list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - yield(list_all_task, "task_finished") - _print_to_console(list_all_task.data) - delete_task = Firebase.Storage.ref("Firebasetester/upload/junkdata").delete() - yield(delete_task, "task_finished") + await list_all_current() + delete = await Firebase.Storage.ref("Firebasetester/upload/junkdata").delete() _print_to_console("After Delete...") - list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - yield(list_all_task, "task_finished") - _print_to_console(list_all_task.data) - $string_delete_check.pressed = true + await list_all_current() + $string_delete_check.button_pressed = true # If nothing has failed to this point, finish the test successfully _print_to_console("\nFINISHED STORAGE TESTS") _test_finished() +func list_all_current() -> void: + var list_all = await Firebase.Storage.ref("Firebasetester").list_all() + _print_to_console(list_all) + # Function used to the the image data for a file an image file storage func get_image(requested_image): - return Firebase.Storage.ref("Firebasetester/upload/{image}".format({image = requested_image})).get_data() + return await Firebase.Storage.ref("Firebasetester/upload/{image}".format({image = requested_image})).get_data() # Fucntion used to convert the data to an image -func task2image(task : StorageTask) -> ImageTexture: +func variant2image(vari : Variant) -> ImageTexture: var new_image := Image.new() - match typeof(task.data): - TYPE_RAW_ARRAY: - var data : PoolByteArray = task.data + match typeof(vari): + TYPE_PACKED_BYTE_ARRAY: + var data : PackedByteArray = vari if data.size()>1: - match data.subarray(0,1).hex_encode(): - "ffd8": + var image_marker = data.slice(0, 1) + var hex = image_marker.hex_encode() + match hex: + "ffd8": # I do not know if this has to change as below; we could add a test for this by also uploading/deleting a jpeg new_image.load_jpg_from_buffer(data) - "8950": + "89": # This apparently had to change as we were getting corrupt data new_image.load_png_from_buffer(data) TYPE_DICTIONARY: - _print_to_console_error("ERROR %s: could not find image requested" % task.data.error.code) + _print_to_console_error("ERROR %s: could not find image requested" % vari) var new_texture := ImageTexture.new() new_texture.create_from_image(new_image) return new_texture @@ -189,18 +171,18 @@ func task2image(task : StorageTask) -> ImageTexture: func _print_to_console(data): data = str(data) print(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + data + "\n" - console.bbcode_text = updated_data + console.text = updated_data # Function used to print error data to the console GUI for the end user func _print_to_console_error(data): data = str(data) printerr(data) - var previous_data = console.bbcode_text + var previous_data = console.text var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_data + console.text = updated_data # Function called when the end user presses the 'Back' button, returns to the Main Menu func _on_back_pressed(): - get_tree().change_scene("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/storage/storage.gd.uid b/tests/storage/storage.gd.uid new file mode 100644 index 0000000..2b2af32 --- /dev/null +++ b/tests/storage/storage.gd.uid @@ -0,0 +1 @@ +uid://e7ht3l1yfhnl diff --git a/tests/storage/storage.tscn b/tests/storage/storage.tscn index 3dc6d67..db3607a 100644 --- a/tests/storage/storage.tscn +++ b/tests/storage/storage.tscn @@ -1,379 +1,318 @@ -[gd_scene load_steps=11 format=2] - -[ext_resource path="res://tests/storage/storage.gd" type="Script" id=1] -[ext_resource path="res://assets/buttons/normal_button.png" type="Texture" id=2] -[ext_resource path="res://assets/background.png" type="Texture" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="DynamicFont" id=4] -[ext_resource path="res://assets/buttons/normal_button.tscn" type="PackedScene" id=5] -[ext_resource path="res://assets/buttons/normal_button_disabled.png" type="Texture" id=6] -[ext_resource path="res://assets/buttons/normal_button.gd" type="Script" id=7] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="DynamicFontData" id=8] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="DynamicFont" id=9] - - -[sub_resource type="DynamicFont" id=1] -size = 26 -font_data = ExtResource( 8 ) - -[node name="storage" type="Node2D"] -script = ExtResource( 1 ) +[gd_scene load_steps=7 format=3 uid="uid://c2rugy5ntvcp2"] + +[ext_resource type="Script" path="res://tests/storage/storage.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://dey7w6ieyntb" path="res://assets/buttons/normal_button.png" id="2"] +[ext_resource type="Texture2D" uid="uid://dg74q2h440e5k" path="res://assets/background.png" id="3"] +[ext_resource type="PackedScene" uid="uid://fnf1abx32uum" path="res://assets/buttons/normal_button.tscn" id="5"] +[ext_resource type="Texture2D" uid="uid://d20ruxpv04p8y" path="res://assets/buttons/normal_button_disabled.png" id="6"] +[ext_resource type="Script" path="res://assets/buttons/normal_button.gd" id="7"] + +[node name="storage" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 -texture = ExtResource( 3 ) -__meta__ = { -"_edit_use_anchors_": false -} +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("3") [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 12.0 -margin_right = 557.0 -margin_bottom = 56.0 -custom_fonts/font = ExtResource( 4 ) +layout_mode = 0 +offset_left = 376.0 +offset_top = 12.0 +offset_right = 557.0 +offset_bottom = 56.0 text = "Storage Tests" -align = 1 +horizontal_alignment = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 5 )] -margin_right = 190.354 -margin_bottom = 44.6465 +[node name="back" parent="." instance=ExtResource("5")] +layout_mode = 1 +offset_right = 190.354 +offset_bottom = 44.6465 label = "Back" [node name="test_storage" type="TextureButton" parent="."] +layout_mode = 1 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -margin_top = 65.0 -margin_right = 190.0 -margin_bottom = 110.0 -texture_normal = ExtResource( 2 ) -texture_disabled = ExtResource( 6 ) -script = ExtResource( 7 ) -__meta__ = { -"_edit_use_anchors_": false -} +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 +grow_horizontal = 2 +grow_vertical = 2 +texture_normal = ExtResource("2") +texture_disabled = ExtResource("6") +script = ExtResource("7") label = "Test Storage" [node name="label" type="RichTextLabel" parent="test_storage"] -margin_left = 10.0 -margin_right = 180.0 -margin_bottom = 45.0 +layout_mode = 0 +offset_left = 10.0 +offset_right = 180.0 +offset_bottom = 45.0 mouse_filter = 2 -custom_fonts/normal_font = SubResource( 1 ) bbcode_enabled = true -fit_content_height = true -__meta__ = { -"_edit_lock_": true -} [node name="image" type="TextureRect" parent="."] -margin_left = 40.0 -margin_top = 450.0 -margin_right = 104.0 -margin_bottom = 514.0 -__meta__ = { -"_edit_use_anchors_": false -} +layout_mode = 0 +offset_left = 40.0 +offset_top = 450.0 +offset_right = 104.0 +offset_bottom = 514.0 [node name="console" type="RichTextLabel" parent="."] -margin_left = 250.586 -margin_top = 117.012 -margin_right = 946.586 -margin_bottom = 522.012 +layout_mode = 0 +offset_left = 250.586 +offset_top = 117.012 +offset_right = 946.586 +offset_bottom = 522.012 bbcode_enabled = true scroll_following = true -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="login_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 131.0 -margin_right = 34.0 -margin_bottom = 155.0 + +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Login" -[node name="upload_image_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 155.0 -margin_right = 34.0 -margin_bottom = 179.0 +[node name="upload_image_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="upload_image_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Upload Image" -[node name="download_image_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 179.0 -margin_right = 34.0 -margin_bottom = 203.0 +[node name="download_image_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 179.0 +offset_right = 34.0 +offset_bottom = 203.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="download_image_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Download Image" -[node name="image_url_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 203.0 -margin_right = 34.0 -margin_bottom = 227.0 +[node name="image_url_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 203.0 +offset_right = 34.0 +offset_bottom = 227.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="image_url_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Image URL" -[node name="image_meta_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 227.0 -margin_right = 34.0 -margin_bottom = 251.0 +[node name="image_meta_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 227.0 +offset_right = 34.0 +offset_bottom = 251.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="image_meta_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Image Metadata" -[node name="image_delete_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 251.0 -margin_right = 34.0 -margin_bottom = 275.0 +[node name="image_delete_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 251.0 +offset_right = 34.0 +offset_bottom = 275.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="image_delete_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Delete Image" -[node name="upload_document_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 275.0 -margin_right = 34.0 -margin_bottom = 299.0 +[node name="upload_document_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 275.0 +offset_right = 34.0 +offset_bottom = 299.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="upload_document_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Upload Document" -[node name="document_meta_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 299.0 -margin_right = 34.0 -margin_bottom = 323.0 +[node name="document_meta_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 299.0 +offset_right = 34.0 +offset_bottom = 323.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="document_meta_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Document Metadata" -[node name="document_delete_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 323.0 -margin_right = 34.0 -margin_bottom = 347.0 +[node name="document_delete_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 323.0 +offset_right = 34.0 +offset_bottom = 347.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="document_delete_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Delete Document" -[node name="upload_string_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 347.0 -margin_right = 34.0 -margin_bottom = 371.0 +[node name="upload_string_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 347.0 +offset_right = 34.0 +offset_bottom = 371.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="upload_string_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Upload String" -[node name="string_add_meta_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 371.0 -margin_right = 34.0 -margin_bottom = 395.0 +[node name="string_add_meta_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 371.0 +offset_right = 34.0 +offset_bottom = 395.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="string_add_meta_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Add Metadata" -[node name="string_meta_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 395.0 -margin_right = 34.0 -margin_bottom = 419.0 +[node name="string_meta_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 395.0 +offset_right = 34.0 +offset_bottom = 419.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="string_meta_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "String Metadata" -[node name="string_delete_check" type="CheckBox" parent="." groups=[ -"tests", -]] -margin_left = 10.0 -margin_top = 419.0 -margin_right = 34.0 -margin_bottom = 443.0 +[node name="string_delete_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 +offset_left = 10.0 +offset_top = 419.0 +offset_right = 34.0 +offset_bottom = 443.0 mouse_filter = 2 disabled = true button_mask = 0 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Label" type="Label" parent="string_delete_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) +layout_mode = 0 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 text = "Delete String" [connection signal="pressed" from="back" to="." method="_on_back_pressed"]