From ef7ee15a8ecb5be3a932132dd3223f579ebeeb15 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 4 Feb 2023 09:49:59 -0500 Subject: [PATCH 01/22] Updating gitignore for new Godot structure --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5cfc054..23589f4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ export_presets.cfg .mono/ data_*/ addons/godot-firebase/.env -*.DS_Store \ No newline at end of file +*.DS_Store +.godot/ \ No newline at end of file From c769d56a6de272dc0503ff526ef91672d86a91e4 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 4 Feb 2023 14:18:29 -0500 Subject: [PATCH 02/22] Attempt to get 4.0 working --- addons/godot-firebase/Utilies.gd | 10 + addons/godot-firebase/auth/auth.gd | 250 ++++++++------ addons/godot-firebase/auth/auth_provider.gd | 4 +- .../godot-firebase/auth/providers/facebook.gd | 4 +- .../godot-firebase/auth/providers/github.gd | 4 +- .../godot-firebase/auth/providers/google.gd | 2 +- .../godot-firebase/auth/providers/twitter.gd | 12 +- addons/godot-firebase/auth/user_data.gd | 8 +- addons/godot-firebase/database/database.gd | 2 +- .../godot-firebase/database/database_store.gd | 8 +- addons/godot-firebase/database/reference.gd | 44 +-- addons/godot-firebase/database/resource.gd | 4 +- .../dynamiclinks/dynamiclinks.gd | 33 +- addons/godot-firebase/firebase/firebase.gd | 43 ++- addons/godot-firebase/firebase/firebase.tscn | 2 +- addons/godot-firebase/firestore/firestore.gd | 126 ++----- .../firestore/firestore_collection.gd | 54 ++- .../firestore/firestore_document.gd | 18 +- .../firestore/firestore_query.gd | 11 +- .../firestore/firestore_task.gd | 256 ++------------ .../godot-firebase/functions/function_task.gd | 42 +-- addons/godot-firebase/functions/functions.gd | 38 +-- addons/godot-firebase/icon.svg.import | 32 +- addons/godot-firebase/plugin.cfg | 6 +- addons/godot-firebase/plugin.gd | 2 +- addons/godot-firebase/storage/storage.gd | 78 ++--- .../storage/storage_reference.gd | 45 ++- addons/godot-firebase/storage/storage_task.gd | 20 +- addons/http-sse-client/HTTPSSEClient.gd | 31 +- .../http-sse-client/httpsseclient_plugin.gd | 2 +- addons/http-sse-client/icon.png.import | 32 +- assets/background.png.import | 32 +- assets/buttons/normal_button.gd | 4 +- assets/buttons/normal_button.png.import | 32 +- assets/buttons/normal_button.tscn | 20 +- .../buttons/normal_button_disabled.png.import | 32 +- assets/image.png.import | 32 +- default_env.tres | 2 +- fonts/PermanentMarker.ttf.import | 33 ++ fonts/PermanentMarker_18.tres | 4 +- fonts/PermanentMarker_30.tres | 4 +- fonts/PermanentMarker_72.tres | 4 +- icon.png.import | 32 +- main.gd | 12 +- main.tscn | 125 ++----- project.godot | 111 +------ tests/auth/auth.gd | 256 +++++++------- tests/auth/auth.tscn | 166 +++++----- tests/database/database.gd | 140 ++++---- tests/database/database.tscn | 116 +++---- tests/firestore/firestore.gd | 228 ++++++------- tests/firestore/firestore.tscn | 198 +++++------ tests/links/links.gd | 114 +++---- tests/links/links.tscn | 84 ++--- tests/storage/storage.gd | 312 +++++++++--------- tests/storage/storage.tscn | 266 +++++++-------- 56 files changed, 1631 insertions(+), 1951 deletions(-) create mode 100644 addons/godot-firebase/Utilies.gd create mode 100644 fonts/PermanentMarker.ttf.import diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd new file mode 100644 index 0000000..1e66a0c --- /dev/null +++ b/addons/godot-firebase/Utilies.gd @@ -0,0 +1,10 @@ +extends Node +class_name Utilities + +static func get_json_data(value): + var json = JSON.new() + var json_parse_result = json.parse(value) + if json_parse_result == OK: + return json.data + + return null diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 549d7de..55dd786 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,11 +50,11 @@ 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 = [ +var _headers : PackedStringArray = [ "Content-Type: application/json", "Accept: application/json", ] @@ -70,7 +70,7 @@ enum Requests { var auth_request_type : int = -1 enum Auth_Type { - NONE = -1 + NONE = -1, LOGIN_EP, LOGIN_ANON, LOGIN_CT, @@ -145,7 +145,7 @@ var _local_provider : AuthProvider = AuthProvider.new() func _ready() -> void: tcp_timer.wait_time = tcp_timeout - tcp_timer.connect("timeout", self, "_tcp_stream_timer") + tcp_timer.timeout.connect(_tcp_stream_timer) if OS.get_name() == "HTML5": _local_uri += "tmp_js_export.html" @@ -165,7 +165,7 @@ func _set_config(config_json : Dictionary) -> void: _delete_account_request_url %= _config.apiKey _update_account_request_url %= _config.apiKey - connect("request_completed", self, "_on_FirebaseAuth_request_completed") + request_completed.connect(_on_FirebaseAuth_request_completed) _check_emulating() @@ -194,7 +194,7 @@ func _is_ready() -> bool: # 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 @@ -212,7 +212,12 @@ func signup_with_email_and_password(email : String, password : String) -> void: _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)) + 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() @@ -223,8 +228,10 @@ 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)) - + 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 @@ -235,7 +242,12 @@ func login_with_email_and_password(email : String, password : String) -> void: _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)) + 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 @@ -244,15 +256,17 @@ func login_with_custom_token(token : String) -> void: 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)) - + 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") + await get_tree().create_timer(0.5).timeout if has_child == false: add_child(tcp_timer) has_child = true @@ -267,7 +281,7 @@ func get_auth_with_redirect(provider: AuthProvider) -> void: 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 + '")') + 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) @@ -282,22 +296,26 @@ func get_auth_with_redirect(provider: AuthProvider) -> void: # 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: @@ -311,10 +329,11 @@ func exchange_token(code : String, redirect_uri : String, request_url: String, _ grant_type = "authorization_code", } requesting = Requests.EXCHANGE_TOKEN - request(request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(exchange_token_body)) + 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 @@ -322,8 +341,7 @@ 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) - -# 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: @@ -334,34 +352,35 @@ func _tcp_stream_timer() -> void: has_child = false var token : String = "" for value in raw_result.split(" ")[1].lstrip("/?").split("&"): - var splitted: PoolStringArray = value.split("=") + var splitted: PackedStringArray = value.split("=") if _local_provider.params.response_type in splitted[0]: token = splitted[1] break + if token == "": - emit_signal("login_failed") + login_failed.emit() 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()) + 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) - yield(self, "login_succeeded") + await 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 +# 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") + logged_out.emit() # Function is called when requesting a manual token refresh @@ -375,13 +394,16 @@ func manual_token_refresh(auth_data): 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)) + 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) +func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + var json = Utilities.get_json_data(body) is_busy = false var res if response_code == 0: @@ -392,47 +414,46 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade "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: + if json == null: Firebase._printerr("Error while parsing auth body json") - emit_signal("auth_request", ERR_PARSE_ERROR, "Error while parsing auth body json") + auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json") return - res = json_result.result + + res = json 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) + token_exchanged.emit(true) begin_refresh_countdown() # Refresh token countdown - emit_signal("auth_request", 1, auth) + auth_request.emit(1, auth) else: match res.kind: RESPONSE_SIGNUP: auth = get_clean_keys(res) - emit_signal("signup_succeeded", auth) + signup_succeeded.emit(auth) begin_refresh_countdown() RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: auth = get_clean_keys(res) - emit_signal("login_succeeded", auth) + login_succeeded.emit(auth) begin_refresh_countdown() RESPONSE_USERDATA: var userdata = FirebaseUserData.new(res.users[0]) - emit_signal("userdata_received", userdata) - emit_signal("auth_request", 1, auth) + 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: - emit_signal("token_exchanged", false) - emit_signal("login_failed", res.error, res.error_description) - emit_signal("auth_request", res.error, res.error_description) + 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" - emit_signal(sig, res.error.code, res.error.message) - emit_signal("auth_request", res.error.code, res.error.message) + 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 @@ -440,33 +461,35 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade # 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)) + 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(to_json(auth)) + encrypted_file.store_line(JSON.stringify(auth)) encrypted_file.close() # 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.") + 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 Firebase Auth File. Error Code: " + str(FileAccess.get_open_error())) + auth_request.emit(err, "Error Opening Firebase Auth File.") else: - var encrypted_file_data = parse_json(encrypted_file.get_line()) - manual_token_refresh(encrypted_file_data) + 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) -# Function used to remove the local encrypted auth file +# 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") + if (FileAccess.file_exists("user://user.auth")): + DirAccess.remove_absolute("user://user.auth") else: Firebase._printerr("No encrypted auth file exists") @@ -474,13 +497,12 @@ func remove_auth() -> void: # 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")): + if (FileAccess.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") + auth_request.emit(ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") # Function used to change the email account for the currently logged in user @@ -489,7 +511,10 @@ func change_user_email(email : String) -> void: 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)) + 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 @@ -498,11 +523,14 @@ func change_user_password(password : String) -> void: 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)) + 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: +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 @@ -510,7 +538,10 @@ func update_account(idToken : String, displayName : String, photoUrl : String, d _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)) + 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 @@ -518,7 +549,10 @@ 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)) + 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. @@ -527,7 +561,10 @@ 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)) + 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 @@ -539,14 +576,20 @@ func get_user_data() -> void: 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})) + 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) # 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. @@ -563,21 +606,24 @@ func begin_refresh_countdown() -> void: 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") + token_refresh_succeeded.emit(auth) + await 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 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 token = JavaScriptBridge.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]);""") + JavaScriptBridge.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") return token return null @@ -589,7 +635,7 @@ func set_local_provider(provider : AuthProvider) -> void: 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 = {} 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/providers/facebook.gd b/addons/godot-firebase/auth/providers/facebook.gd index 32c1290..9dc4b1d 100644 --- a/addons/godot-firebase/auth/providers/facebook.gd +++ b/addons/godot-firebase/auth/providers/facebook.gd @@ -1,7 +1,7 @@ class_name FacebookProvider 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,7 +10,7 @@ func _init(client_id: String, client_secret: String) -> void: 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)) + self.params.state = str(randf_range(0, 1)) if OS.get_name() == "HTML5": self.should_exchange = false self.params.response_type = "token" 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/google.gd b/addons/godot-firebase/auth/providers/google.gd index 48ad38f..2ea88cf 100644 --- a/addons/godot-firebase/auth/providers/google.gd +++ b/addons/godot-firebase/auth/providers/google.gd @@ -1,7 +1,7 @@ class_name GoogleProvider extends AuthProvider -func _init(client_id: String, client_secret: String) -> void: +func _init(client_id: String,client_secret: String): set_client_id(client_id) set_client_secret(client_secret) self.should_exchange = true 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/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/database/database.gd b/addons/godot-firebase/database/database.gd index 1d51b91..430b33a 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 diff --git a/addons/godot-firebase/database/database_store.gd b/addons/godot-firebase/database/database_store.gd index 07b6753..e6ea028 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 @@ -54,14 +54,14 @@ func _update_data(path: String, payload, patch: bool) -> void: # Traverse the path. # var dict = _data - var keys = PoolStringArray([_ROOT]) + var keys = PackedStringArray([_ROOT]) keys.append_array(path.split(_DELIMITER, false)) 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): diff --git a/addons/godot-firebase/database/reference.gd b/addons/godot-firebase/database/reference.gd index b4ae5c8..0c485cd 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -2,7 +2,7 @@ ## @meta-version 2.3 ## A reference to a location in the Realtime Database. ## Documentation TODO. -tool +@tool class_name FirebaseDatabaseReference extends Node @@ -48,7 +48,7 @@ const _escaped_quote : String = '"' const _equal_tag : String = "=" const _key_filter_tag : String = "$key" -var _headers : PoolStringArray = [] +var _headers : PackedStringArray = [] func set_db_path(path : String, filter_query_dict : Dictionary) -> void: _db_path = path @@ -62,13 +62,13 @@ func set_pusher(pusher_ref : HTTPRequest) -> void: if !_pusher: _pusher = pusher_ref add_child(_pusher) - _pusher.connect("request_completed", self, "on_push_request_complete") + _pusher.request_completed.connect(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") + _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 @@ -84,13 +84,13 @@ func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) - 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])) + new_data_update.emit(FirebaseResource.new(_separator + key, data.data[key])) elif data.path != _separator: - emit_signal("new_data_update", FirebaseResource.new(data.path, data.data)) + new_data_update.emit(FirebaseResource.new(data.path, data.data)) elif command == _patch_tag: - emit_signal("patch_data_update", FirebaseResource.new(data.path, data.data)) + patch_data_update.emit(FirebaseResource.new(data.path, data.data)) elif command == _delete_tag: - emit_signal("delete_data_update", FirebaseResource.new(data.path, data.data)) + delete_data_update.emit(FirebaseResource.new(data.path, data.data)) pass func set_store(store_ref : FirebaseDatabaseStore) -> void: @@ -104,25 +104,25 @@ func update(path : String, data : Dictionary) -> void: if path == _separator: path = "" - var to_update = JSON.print(data) + var to_update = JSON.stringify(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) + _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) else: _update_queue.append({"path": path, "data": data}) func push(data : Dictionary) -> void: - var to_push = JSON.print(data) + var to_push = JSON.stringify(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) + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, 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, "") + _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") else: _delete_queue.append(reference) @@ -138,7 +138,7 @@ func get_data() -> Dictionary: func _get_remaining_path(is_push : bool = true) -> String: var remaining_path = "" - if !_filter_query or is_push: + 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 @@ -156,10 +156,10 @@ func _get_list_url(with_port:bool = true) -> String: func _get_filter(): - if !_filter_query: + 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: + 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 @@ -171,6 +171,9 @@ func _get_filter(): 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. @@ -183,18 +186,21 @@ func _route_data(command : String, path : String, data) -> void: elif command == _delete_tag: _store.delete(path, data) -func on_push_request_complete(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: +func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: if response_code == HTTPClient.RESPONSE_OK: - emit_signal("push_successful") + push_successful.emit() else: - emit_signal("push_failed") + push_failed.emit() + # handle queued operations 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()) diff --git a/addons/godot-firebase/database/resource.gd b/addons/godot-firebase/database/resource.gd index c273206..ff5b985 100644 --- a/addons/godot-firebase/database/resource.gd +++ b/addons/godot-firebase/database/resource.gd @@ -1,14 +1,14 @@ ## @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: +func _init(key : String,data): self.key = key.lstrip("/") self.data = data diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index fe6e4a3..c23e86c 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,7 +22,7 @@ var _config : Dictionary = {} var _auth : Dictionary var _request_list_node : HTTPRequest -var _headers : PoolStringArray = [] +var _headers : PackedStringArray = [] enum Requests { NONE = -1, @@ -32,7 +32,7 @@ enum Requests { 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") + _request_list_node.request_completed.connect(_on_request_completed) add_child(_request_list_node) _check_emulating() @@ -64,15 +64,15 @@ var _link_request_body : Dictionary = { "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!") + 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 @@ -84,17 +84,18 @@ func generate_dynamic_link(long_link : String, APN : String, IBI : String, is_un _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 + _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: - result_body = result_body.result - - emit_signal("dynamic_link_generated", result_body.shortLink) + 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: diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index d53d0b7..8c106b3 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -8,7 +8,7 @@ ## - [code]Storage[/code]: Gives access to Cloud Storage; perfect for storing files like images and other assets. ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki -tool +@tool extends Node const _ENVIRONMENT_VARIABLES : String = "firebase/environment_variables" @@ -17,33 +17,33 @@ 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 +@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": "", @@ -97,19 +97,18 @@ func _check_emulating() -> void: module._check_emulating() func _load_config() -> void: - if _config.apiKey != "" and _config.authDomain != "": - pass - else: + 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(): - 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) + 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" and config_value.has("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 == "": @@ -126,10 +125,10 @@ func _setup_modules() -> void: 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") + 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) # ------------- diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index 6b86131..3bc100d 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -9,7 +9,7 @@ [ext_resource path="res://addons/godot-firebase/functions/functions.gd" type="Script" id=7] [node name="Firebase" type="Node"] -pause_mode = 2 +process_mode = 2 script = ExtResource( 3 ) [node name="Auth" type="HTTPRequest" parent="."] diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 156a645..4fe44d6 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -12,7 +12,7 @@ ## (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 @@ -31,8 +31,8 @@ signal task_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. + 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 @@ -56,7 +56,7 @@ var persistence_enabled : bool = true ## 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 collections currently referenced. ## @type Dictionary @@ -83,7 +83,7 @@ var _current_query : FirestoreQuery var _http_request_pool := [] -var _offline: bool = false setget _set_offline +var _offline: bool = false : set = _set_offline func _ready() -> void: pass @@ -95,14 +95,15 @@ func _process(delta : float) -> void: var lifetime: float = request.get_meta("lifetime") + delta if lifetime > _MAX_POOLED_REQUEST_AGE: request.queue_free() - _http_request_pool.remove(i) + _http_request_pool.remove_at(i) + continue # Just to skip set_meta on a queue_freed request request.set_meta("lifetime", lifetime) ## 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: @@ -120,19 +121,19 @@ func collection(path : String) -> FirestoreCollection: return collections[path] -## Issue a query on your Firestore database. +## 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. +## If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. ## ## ex. ## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code] -## [code]yield(query_task, "task_finished")[/code] +## [code]await 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 result : Array = await query_task.task_finished[/code] ## ## [b]Warning:[/b] It currently does not work offline! ## @@ -141,28 +142,28 @@ func collection(path : String) -> FirestoreCollection: ## @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.result_query.connect(_on_result_query) # In theory, this and the following could be a CONNECT_ONE_SHOT, but I'm iffy on whether or not that might break any client code, so leaving this for now. + firestore_task.task_error.connect(_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._fields = JSON.stringify(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. +## 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 checked 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] +## [code]await 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 result : Array = await query_task.task_finished[/code] ## ## @args collection_id, page_size, page_token, order_by ## @arg-types String, int, String, String @@ -170,8 +171,8 @@ func query(query : FirestoreQuery) -> FirestoreTask: ## @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.listed_documents.connect(_on_listed_documents) # Same as above with one shot connections + firestore_task.task_error.connect(_on_task_error) firestore_task.action = FirestoreTask.Task.TASK_LIST var url : String = _base_url + _extended_url + path if page_size != 0: @@ -214,63 +215,7 @@ func disable_networking() -> void: 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 # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. func _set_config(config_json : Dictionary) -> void: @@ -278,11 +223,7 @@ func _set_config(config_json : Dictionary) -> void: _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 + # 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() @@ -300,19 +241,19 @@ func _check_emulating() -> void : func _pooled_request(task : FirestoreTask) -> void: if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) return - if not auth and not Firebase.emulating: + if (auth == null or auth.is_empty()) and not Firebase.emulating: Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") + 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 = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) var http_request : HTTPRequest for request in _http_request_pool: @@ -325,26 +266,25 @@ func _pooled_request(task : FirestoreTask) -> void: 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.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, !Firebase.emulating, task._method, task._fields) + http_request.request(task._url, task._headers, task._method, task._fields) # ------------- -func _on_listed_documents(listed_documents : Array): - emit_signal("listed_documents", listed_documents) - +func _on_listed_documents(_listed_documents : Array): + listed_documents.emit(_listed_documents) func _on_result_query(result : Array): - emit_signal("result_query", result) + result_query.emit(result) func _on_task_error(code : int, status : String, message : String, task : int): - emit_signal("task_error", code, status, message) + task_error.emit(code, status, message) Firebase._printerr(message) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: @@ -359,7 +299,7 @@ func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: collections[key].auth = auth -func _on_pooled_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest) -> void: +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) diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 7903318..8808112 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -3,9 +3,9 @@ ## @meta-version 2.3 ## A reference to a Firestore Collection. ## Documentation TODO. -tool +@tool class_name FirestoreCollection -extends Reference +extends RefCounted signal add_document(doc) signal get_document(doc) @@ -35,14 +35,14 @@ var _request_queues := {} ## @args document_id ## @return FirestoreTask ## used to GET a document from the collection, specify @document_id -func get(document_id : String) -> FirestoreTask: +func get_doc(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) + task.get_document.connect(_on_get_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) _process_request(task, document_id, url) return task @@ -56,9 +56,9 @@ func add(document_id : String, fields : Dictionary = {}) -> FirestoreTask: 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))) + task.add_document.connect(_on_add_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) return task ## @args document_id, fields @@ -74,9 +74,9 @@ func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: 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))) + task.update_document.connect(_on_update_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) return task ## @args document_id @@ -88,8 +88,8 @@ func delete(document_id : String) -> FirestoreTask: 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) + task.delete_document.connect(_on_delete_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) _process_request(task, document_id, url) return task @@ -99,46 +99,44 @@ func _get_request_url() -> String: func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: - task.connect("task_error", self, "_on_error") + if not task.task_error.is_connected(_on_error): + task.task_error.connect(_on_error) - if not auth: + if auth == null or auth.is_empty(): Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") + var result : Array = await Firebase.Auth.auth_request if result[0] != 1: Firebase.Firestore._check_auth_error(result[0], result[1]) - return null + return 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(): + task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + if _request_queues.has(document_id) and not _request_queues[document_id].is_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(): + if not _request_queues[document_id].is_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 ) + get_document.emit(document) func _on_add_document(document : FirestoreDocument): - emit_signal("add_document", document ) + add_document.emit(document) func _on_update_document(document : FirestoreDocument): - emit_signal("update_document", document ) + update_document.emit(document) func _on_delete_document(): - emit_signal("delete_document") + delete_document.emit() func _on_error(code, status, message, task): - emit_signal("error", code, status, message) + error.emit(code, status, message) Firebase._printerr(message) diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index 87e7e51..12039b6 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -2,9 +2,9 @@ ## @meta-version 2.2 ## A reference to a Firestore Document. ## Documentation TODO. -tool +@tool class_name FirestoreDocument -extends Reference +extends RefCounted # A FirestoreDocument objects that holds all important values for a Firestore Document, # @doc_name = name of the Firestore Document, which is the request PATH @@ -16,7 +16,7 @@ var doc_fields : Dictionary # only .fields var doc_name : String # only .name var create_time : String # createTime -func _init(doc : Dictionary = {}, _doc_name : String = "", _doc_fields : Dictionary = {}) -> void: +func _init(doc : Dictionary = {},_doc_name : String = "",_doc_fields : Dictionary = {}): self.document = doc self.doc_name = doc.name if self.doc_name.count("/") > 2: @@ -25,7 +25,7 @@ func _init(doc : Dictionary = {}, _doc_name : String = "", _doc_fields : Diction 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: +# 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 : Dictionary = {} @@ -35,14 +35,14 @@ static func dict2fields(dict : Dictionary) -> Dictionary: if "." in field: var keys: Array = field.split(".") field = keys.pop_front() - keys.invert() + 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_REAL: var_type = "doubleValue" + TYPE_FLOAT: var_type = "doubleValue" TYPE_STRING: var_type = "stringValue" TYPE_DICTIONARY: if is_field_timestamp(field_value): @@ -101,7 +101,7 @@ static func array2fields(array : Array) -> Array: TYPE_NIL: var_type = "nullValue" TYPE_BOOL: var_type = "booleanValue" TYPE_INT: var_type = "integerValue" - TYPE_REAL: var_type = "doubleValue" + TYPE_FLOAT: var_type = "doubleValue" TYPE_STRING: var_type = "stringValue" TYPE_ARRAY: var_type = "arrayValue" @@ -134,7 +134,7 @@ static func fields2array(array : Dictionary) -> Array: fields.append(item) return fields -# Converts a gdscript Dictionary (most likely obtained with OS.get_datetime()) to a Firebase Timestamp +# 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') @@ -144,7 +144,7 @@ static func dict2timestamp(dict : Dictionary) -> String: # 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("-") + 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]) diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index a75a692..6a22e81 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -2,8 +2,8 @@ ## @meta-version 1.4 ## A firestore query. ## Documentation TODO. -tool -extends Reference +@tool +extends RefCounted class_name FirestoreQuery class Order: @@ -13,7 +13,7 @@ class Cursor: var values : Array var before : bool - func _init(v : Array, b : bool): + func _init(v : Array,b : bool): values = v before = b @@ -63,9 +63,6 @@ enum DIRECTION { 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 @@ -107,7 +104,7 @@ 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")): diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index ad65f03..3fe53a4 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -4,42 +4,42 @@ ## 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. +## Emitted when a [code]add(document)[/code] request checked 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. +## Emitted when a [code]get(document)[/code] request checked 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. +## Emitted when a [code]update(document)[/code] request checked 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. +## Emitted when a [code]delete(document)[/code] request checked 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. +## Emitted when a [code]list(collection_id)[/code] request checked [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. +## Emitted when a [code]query(collection_id)[/code] request checked [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. @@ -58,7 +58,7 @@ enum Task { ## 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 @@ -67,81 +67,44 @@ var document : FirestoreDocument ## 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 _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 +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 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 - + # Probably going to regret this... if response_code == HTTPClient.RESPONSE_OK: data = bod match action: Task.TASK_POST: document = FirestoreDocument.new(bod) - emit_signal("add_document", document) + add_document.emit(document) Task.TASK_GET: document = FirestoreDocument.new(bod) - emit_signal("get_document", document) + get_document.emit(document) Task.TASK_PATCH: document = FirestoreDocument.new(bod) - emit_signal("update_document", document) + update_document.emit(document) Task.TASK_DELETE: - emit_signal("delete_document") + delete_document.emit() Task.TASK_QUERY: data = [] for doc in bod: if doc.has('document'): data.append(FirestoreDocument.new(doc.document)) - emit_signal("result_query", data) + result_query.emit(data) Task.TASK_LIST: data = [] if bod.has('documents'): @@ -149,25 +112,25 @@ func _on_request_completed(result : int, response_code : int, headers : PoolStri data.append(FirestoreDocument.new(doc)) if bod.has("nextPageToken"): data.append(bod.nextPageToken) - emit_signal("listed_documents", data) + listed_documents.emit(data) else: Firebase._printerr("Action in error was: " + str(action)) - emit_error("task_error", bod, action) + emit_error(task_error, bod, action) - emit_signal("task_finished", self) + task_finished.emit(self) -func emit_error(signal_name : String, bod, task) -> void: +func emit_error(_signal, 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) + _signal.emit(error.code, error.status, error.message, task) return - emit_signal(signal_name, 1, 0, "Unknown error", task) + _signal.emit(1, 0, "Unknown error", task) func set_action(value : int) -> void: action = value @@ -183,137 +146,7 @@ func set_action(value : int) -> void: 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 - + return body # Removing caching for now, hopefully this works without killing everyone and everything func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: var ret := dic_a.duplicate(true) @@ -340,7 +173,7 @@ func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: var index : int = i - deletions var val = arr_b[index] if val == null and nullify: - ret.remove(index) + ret.remove_at(index) deletions += i elif val is Array: ret[index] = _merge_array(ret[index] if ret[index] else [], val) @@ -349,24 +182,3 @@ func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: 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 diff --git a/addons/godot-firebase/functions/function_task.gd b/addons/godot-firebase/functions/function_task.gd index f82e4dd..ba35b09 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 data = bod if response_code == HTTPClient.RESPONSE_OK and data!=null: - emit_signal("function_executed", result, data) + function_executed.emit(result, data) else: error = {result=result, response_code=response_code, data=data} - emit_signal("task_error", result, response_code, str(data)) - - emit_signal("task_finished", data) + task_error.emit(result, response_code, str(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/functions.gd b/addons/godot-firebase/functions/functions.gd index 18fbb96..e7f2b5d 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 @@ -34,7 +34,7 @@ var persistence_enabled : bool = true ## 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 @@ -48,7 +48,7 @@ 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 @@ -60,7 +60,8 @@ func _process(delta : float) -> void: var lifetime: float = request.get_meta("lifetime") + delta if lifetime > _MAX_POOLED_REQUEST_AGE: request.queue_free() - _http_request_pool.remove(i) + _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) @@ -68,23 +69,23 @@ func _process(delta : float) -> void: ## @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") + 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 var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function function_task._url = url - if not params.empty(): + 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._headers = PackedStringArray(["Content-Type: application/json"]) + function_task._fields = JSON.stringify(body) _pooled_request(function_task) return function_task @@ -145,13 +146,13 @@ func _check_emulating() -> void : func _pooled_request(task : FunctionTask) -> void: if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) return - if not auth: + if auth == null or auth.is_empty(): Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") + 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") @@ -169,12 +170,12 @@ func _pooled_request(task : FunctionTask) -> void: 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]) + 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.request(task._url, task._headers, task._method, task._fields) # ------------- @@ -186,7 +187,7 @@ func _on_function_executed(result : int, data : Dictionary) : pass func _on_task_error(code : int, status : int, message : String): - emit_signal("task_error", code, status, message) + task_error.emit(code, status, message) Firebase._printerr(message) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: @@ -197,14 +198,13 @@ func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: auth = auth_result -func _on_pooled_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest) -> void: +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) func _on_FirebaseAuth_logout() -> void: 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..5636e73 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.0" script="plugin.gd" diff --git a/addons/godot-firebase/plugin.gd b/addons/godot-firebase/plugin.gd index d21b2e2..f68d5ae 100644 --- a/addons/godot-firebase/plugin.gd +++ b/addons/godot-firebase/plugin.gd @@ -1,4 +1,4 @@ -tool +@tool extends EditorPlugin func _enter_tree() -> void: diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 56da0ea..3fcf62b 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -4,18 +4,18 @@ ## 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-types int, int, PackedStringArray ## @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,8 +41,8 @@ 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 @@ -59,7 +59,7 @@ func _internal_process(_delta : float) -> void: match _http_client.get_status(): HTTPClient.STATUS_DISCONNECTED: - _http_client.connect_to_host(_base_url, 443, true) + _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, \ @@ -76,7 +76,7 @@ func _internal_process(_delta : float) -> void: _reading_body = true # If there is a response... - if _response_headers.empty(): + if _response_headers.is_empty(): _response_headers = _http_client.get_response_headers() # Get response headers. _response_code = _http_client.get_response_code() @@ -107,15 +107,15 @@ func _internal_process(_delta : float) -> void: _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) + 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: + if _config == null or _config.is_empty(): return null # Create a root storage reference if there's none @@ -131,7 +131,7 @@ func ref(path := "") -> StorageReference: ref.bucket = bucket ref.full_path = path ref.name = path.get_file() - ref.parent = ref(path.plus_file("..")) + ref.parent = ref(path.path_join("..")) ref.root = _root_ref ref.storage = self return ref @@ -158,8 +158,8 @@ func _check_emulating() -> void : _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): +func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> StorageTask: + if _is_invalid_authentication(): return null var task := StorageTask.new() @@ -172,7 +172,7 @@ func _upload(data : PoolByteArray, headers : PoolStringArray, ref : StorageRefer return task func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> StorageTask: - if not (_config and _auth): + if _is_invalid_authentication(): return null var info_task := StorageTask.new() @@ -190,23 +190,23 @@ func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Sto 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 + var data = await info_task.task_finished + if info_task.result == OK: + task._url += data.downloadTokens # I don't see how this will ever work, but who knows; pretty sure it doesn't, which means in theory it should be running the else every single time; data is a PackedByteArray, not sure what the original code was smoking 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) + task.task_finished.emit() + task_failed.emit(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): + if _is_invalid_authentication(): return null var task := StorageTask.new() @@ -217,7 +217,7 @@ func _list(ref : StorageReference, list_all : bool) -> StorageTask: return task func _delete(ref : StorageReference) -> StorageTask: - if not (_config and _auth): + if _is_invalid_authentication(): return null var task := StorageTask.new() @@ -235,12 +235,12 @@ func _process_request(task : StorageTask) -> void: var headers = Array(task._headers) headers.append("Authorization: Bearer " + _auth.idtoken) - task._headers = PoolStringArray(headers) + task._headers = PackedStringArray(headers) _current_task = task _response_code = 0 - _response_headers = PoolStringArray() - _response_data = PoolByteArray() + _response_headers = PackedStringArray() + _response_data = PackedByteArray() _content_length = 0 _reading_body = false @@ -263,20 +263,20 @@ func _finish_request(result : int) -> void: StorageTask.Task.TASK_DELETE: _references.erase(task.ref.full_path) task.ref.valid = false - if typeof(task.data) == TYPE_RAW_ARRAY: + if typeof(task.data) == TYPE_PACKED_BYTE_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"): + 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 : Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result + var json = Utilities.get_json_data(_response_data) var items := [] - if json and json.has("items"): + if json != null and json.has("items"): for item in json.items: var item_name : String = item.name if item.bucket != bucket: @@ -298,24 +298,25 @@ func _finish_request(result : int) -> void: task.data = items _: - task.data = JSON.parse(_response_data.get_string_from_utf8()).result + var json = Utilities.get_json_data(_response_data) + task.data = json var next_task : StorageTask - if not _pending_tasks.empty(): + if not _pending_tasks.is_empty(): next_task = _pending_tasks.pop_front() task.finished = true - task.emit_signal("task_finished") + task.task_finished.emit() if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): - emit_signal("task_failed", task.result, task.response_code, task.data) + task_failed.emit(task.result, task.response_code, task.data) else: - emit_signal("task_successful", task.result, task.response_code, task.data) + task_successful.emit(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(): + elif not _pending_tasks.is_empty(): next_task = _pending_tasks.pop_front() else: break @@ -338,7 +339,7 @@ func _simplify_path(path : String) -> String: else: new_dirs.push_back(dir) - var new_path := PoolStringArray(new_dirs).join("/") + var new_path := "/".join(PackedStringArray(new_dirs)) new_path = new_path.replace("//", "/") new_path = new_path.replace("\\", "/") return new_path @@ -351,3 +352,6 @@ func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: func _on_FirebaseAuth_logout() -> void: _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_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 56a41f5..08b1a49 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -2,15 +2,15 @@ ## @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 RefCounted ## 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", @@ -75,12 +75,12 @@ var valid : bool = false 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: +## Makes an attempt to upload data to the referenced file location. Status checked this task is found in the returned [StorageTask]. +func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: if not valid: return null if not "Content-Length" in metadata and OS.get_name() != "HTML5": @@ -96,15 +96,14 @@ func put_data(data : PoolByteArray, metadata := {}) -> StorageTask: ## @return StorageTask ## 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) + return put_data(data.to_utf8_buffer(), metadata) ## @args file_path, metadata ## @return StorageTask ## 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()) + var file := FileAccess.open(file_path, FileAccess.READ) + var data := file.get_buffer(file.get_length()) file.close() if "Content-Type" in metadata: @@ -113,7 +112,7 @@ func put_file(file_path : String, metadata := {}) -> StorageTask: return 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]. +## 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() -> StorageTask: if not valid: return null @@ -124,18 +123,18 @@ func get_data() -> 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"]) + task.task_finished.connect(_on_task_finished.bind(task, "stringify")) return task ## @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]. +## 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() -> StorageTask: if not valid: return null return 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]. +## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask]. func get_metadata() -> StorageTask: if not valid: return null @@ -143,30 +142,30 @@ func get_metadata() -> StorageTask: ## @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]. +## 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) -> StorageTask: if not valid: return null - var data := JSON.print(metadata).to_utf8() - var headers := PoolStringArray(["Accept: application/json"]) + var data := JSON.stringify(metadata).to_utf8_buffer() + var headers := PackedStringArray(["Accept: application/json"]) return 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]. +## 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() -> StorageTask: if not valid: return null return 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]. +## 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() -> StorageTask: if not valid: return null return 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]. +## 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() -> StorageTask: if not valid: return null @@ -175,11 +174,11 @@ func delete() -> StorageTask: func _to_string() -> String: var string := "gs://%s/%s" % [bucket, full_path] if not valid: - string += " [Invalid Reference]" + string += " [Invalid RefCounted]" return string func _on_task_finished(task : StorageTask, action : String) -> void: match action: "stringify": - if typeof(task.data) == TYPE_RAW_ARRAY: + if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: task.data = task.data.get_string_from_utf8() diff --git a/addons/godot-firebase/storage/storage_task.gd b/addons/godot-firebase/storage/storage_task.gd index 213b371..ad810c5 100644 --- a/addons/godot-firebase/storage/storage_task.gd +++ b/addons/godot-firebase/storage/storage_task.gd @@ -1,9 +1,9 @@ ## @meta-authors SIsilicon ## @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, @@ -17,22 +17,22 @@ enum Task { 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) +var ref # Storage RefCounted (Can't static type due to cyclic reference) ## @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() +## @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 +47,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,7 +58,7 @@ 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 diff --git a/addons/http-sse-client/HTTPSSEClient.gd b/addons/http-sse-client/HTTPSSEClient.gd index f15b395..aac13bd 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,28 +15,29 @@ 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): +func connect_to_host(domain : String, url_after_domain : String, port : int = -1, trusted_chain : X509Certificate = null, common_name_override : String = ""): self.domain = domain self.url_after_domain = url_after_domain self.port = port - self.use_ssl = use_ssl - self.verify_host = verify_host + 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) + var tls_options = TLSOptions.client(trusted_chain, common_name_override) + var err = httpclient.connect_to_host(domain, port, tls_options) if err == OK: - emit_signal("connected") + connected.emit() is_connected = true else: - emit_signal("connection_error", str(err)) + connection_error.emit(str(err)) func attempt_to_request(httpclient_status): if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING: @@ -52,10 +53,12 @@ func _parse_response_body(headers): 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) + 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) 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/icon.png.import b/addons/http-sse-client/icon.png.import index 9da3f5b..24f6468 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://b21bcvx380sb4" +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..9bc4547 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.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..163c37b 100644 --- a/assets/buttons/normal_button.tscn +++ b/assets/buttons/normal_button.tscn @@ -1,17 +1,17 @@ [gd_scene load_steps=5 format=2] [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 path="res://assets/buttons/normal_button.png" type="Texture2D" id=2] +[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" id=3] +[ext_resource path="res://assets/buttons/normal_button_disabled.png" type="Texture2D" id=4] [node name="normal_button" type="TextureButton"] anchor_right = 1.0 anchor_bottom = 1.0 -margin_left = 0.353546 -margin_top = -0.353546 -margin_right = -529.646 -margin_bottom = -1235.35 +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 ) @@ -20,9 +20,9 @@ __meta__ = { } [node name="label" type="RichTextLabel" parent="."] -margin_left = 10.0 -margin_right = 180.0 -margin_bottom = 45.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 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..abcc65a --- /dev/null +++ b/fonts/PermanentMarker.ttf.import @@ -0,0 +1,33 @@ +[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 +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +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..adcd5c3 100644 --- a/main.gd +++ b/main.gd @@ -1,23 +1,23 @@ extends Node2D func _ready(): - pass + 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") diff --git a/main.tscn b/main.tscn index e636d3b..7189d11 100644 --- a/main.tscn +++ b/main.tscn @@ -1,146 +1,93 @@ -[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" path="res://assets/buttons/normal_button.tscn" id="4"] [node name="main" type="Node2D"] -script = ExtResource( 1 ) +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 -} +offset_right = 1024.0 +offset_bottom = 600.0 +texture = ExtResource("2") [node name="menu" type="VBoxContainer" parent="."] +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -margin_right = 1024.0 -margin_bottom = 600.0 +offset_right = 1024.0 +offset_bottom = 600.0 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="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 ) +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"] diff --git a/project.godot b/project.godot index 0c92cb0..6b5a7ac 100644 --- a/project.godot +++ b/project.godot @@ -6,118 +6,13 @@ ; [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.0") config/icon="res://icon.png" [autoload] @@ -126,7 +21,7 @@ Firebase="*res://addons/godot-firebase/firebase/firebase.tscn" [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] diff --git a/tests/auth/auth.gd b/tests/auth/auth.gd index 120bbc6..d17c581 100644 --- a/tests/auth/auth.gd +++ b/tests/auth/auth.gd @@ -3,10 +3,12 @@ extends Node2D # 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,163 +18,161 @@ 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 # 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.pressed = false - $back.disabled = true - $test_auth.disabled = true + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_auth.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_auth.disabled = false + _test_running = false + $back.disabled = false + $test_auth.disabled = false # Function called when the end user presses the 'Test Auth' button # Main function that is run when testing Authentication func _on_test_auth_pressed() -> void: - # Print to the console GUI that the test is starting - _print_to_console("STARTING AUTH TESTS") - _test_started() - - # Start signup test using the fist 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") - - # Start Login test using the first email - if _auth_error != true: - $signup_check.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") - - # Check Auth File - if _auth_error != true: - $login_check.pressed = true - _check_auth_file() - yield(get_tree().create_timer(_timer_length), "timeout") - - # Get User Data Test - if _auth_error != true: - $auth_file_check.pressed = true - _print_to_console("\nTrying to get user data...") - Firebase.Auth.get_user_data() - yield(get_tree().create_timer(_timer_length), "timeout") - - # Change User Password - if _auth_error != true: - $user_data_check.pressed = true - _print_to_console("\nTrying to change user password...") - Firebase.Auth.change_user_password(_password2) - yield(get_tree().create_timer(_timer_length), "timeout") - Firebase.Auth.logout() - yield(get_tree().create_timer(_timer_length), "timeout") - Firebase.Auth.login_with_email_and_password(_email1, _password2) - yield(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 - _print_to_console("\nTrying to change user email...") - Firebase.Auth.change_user_email(_email2) - yield(get_tree().create_timer(_timer_length), "timeout") - Firebase.Auth.logout() - yield(get_tree().create_timer(_timer_length), "timeout") - Firebase.Auth.login_with_email_and_password(_email2, _password2) - yield(get_tree().create_timer(_timer_length), "timeout") - - # Login with the new credentials - if _auth_error != true: - $change_email_check.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") - - # Start Delete Account test - if _auth_error != true: - $login_check_2.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 - _print_to_console("\nFINISHED AUTH TESTS") - _test_finished() - else: - _auth_test_error() + # Print to the console GUI that the test is starting + _print_to_console("STARTING AUTH TESTS") + _test_started() + + # 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) + await get_tree().create_timer(_timer_length).timeout + + # Start Login test using the first email + if not Firebase.Auth.auth.is_empty(): + $signup_check.button_pressed = true + _print_to_console("\nTrying to login...") + Firebase.Auth.login_with_email_and_password(_email1, _password1) + await get_tree().create_timer(_timer_length).timeout + + # Check Auth File + if not Firebase.Auth.auth.is_empty(): + $login_check.button_pressed = true + _check_auth_file() + await get_tree().create_timer(_timer_length).timeout + + # Get User Data Test + if not Firebase.Auth.auth.is_empty(): + $auth_file_check.button_pressed = true + _print_to_console("\nTrying to get user data...") + Firebase.Auth.get_user_data() + await get_tree().create_timer(_timer_length).timeout + + # Change User Password + if not Firebase.Auth.auth.is_empty(): + $user_data_check.button_pressed = true + _print_to_console("\nTrying to change user password...") + Firebase.Auth.change_user_password(_password2) + await get_tree().create_timer(_timer_length).timeout + Firebase.Auth.logout() + await get_tree().create_timer(_timer_length).timeout + Firebase.Auth.login_with_email_and_password(_email1, _password2) + await get_tree().create_timer(_timer_length).timeout + + # Change the user email from the first email to the second email + if not Firebase.Auth.auth.is_empty(): + $change_pass_check.button_pressed = true + _print_to_console("\nTrying to change user email...") + Firebase.Auth.change_user_email(_email2) + await get_tree().create_timer(_timer_length).timeout + Firebase.Auth.logout() + await get_tree().create_timer(_timer_length).timeout + Firebase.Auth.login_with_email_and_password(_email2, _password2) + await get_tree().create_timer(_timer_length).timeout + + # Login with the new credentials + if not Firebase.Auth.auth.is_empty(): + $change_email_check.button_pressed = true + _print_to_console("\nTrying to login with new creds...") + Firebase.Auth.login_with_email_and_password(_email2, _password2) + await get_tree().create_timer(_timer_length).timeout + + # Start Delete Account test + if not Firebase.Auth.auth.is_empty(): + $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 not Firebase.Auth.auth.is_empty(): + $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("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")): - _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") + _print_to_console("Signup with email and password 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)...") + 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") - Firebase.Auth.save_auth(_auth) + _print_to_console("Login with email and password 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")): - _print_to_console("Auth file exists, good to go") - else: - _print_to_console_error("No encrypted auth file exists") - _auth_error = true + _print_to_console("Checking for auth file (It should be there)...") + 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") + _auth_error = true func _on_userdata_received(userdata) -> void: - if userdata != null: - _print_to_console("User data recieved") - else: - _print_to_console_error("Could not get user data") - _auth_error = true + if userdata != null: + _print_to_console("User data received") + else: + _print_to_console_error("Could not get user data") + _auth_error = true func _on_login_failed(error_code, message): - _print_to_console_error("error code: " + str(error_code)) - _print_to_console_error("message: " + str(message)) - _auth_error = true + _print_to_console_error("error code: " + str(error_code)) + _print_to_console_error("message: " + str(message)) + _auth_error = true func _auth_test_error() -> void: - _print_to_console_error("There has been a failure") - emit_signal("test_finished") + _print_to_console_error("There has been a failure") + 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 updated_data = previous_data + data + "\n" - console.bbcode_text = updated_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.bbcode_text - var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_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("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/auth/auth.tscn b/tests/auth/auth.tscn index 77152b1..e7ac12f 100644 --- a/tests/auth/auth.tscn +++ b/tests/auth/auth.tscn @@ -2,27 +2,27 @@ [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] +[ext_resource path="res://assets/background.png" type="Texture2D" id=3] +[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" id=4] +[ext_resource path="res://fonts/PermanentMarker_18.tres" type="FontFile" id=5] [node name="auth" type="Node2D"] script = ExtResource( 1 ) [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 +offset_right = 1024.0 +offset_bottom = 600.0 texture = ExtResource( 3 ) __meta__ = { "_edit_use_anchors_": false } [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 13.0 -margin_right = 498.0 -margin_bottom = 52.0 +offset_left = 376.0 +offset_top = 13.0 +offset_right = 498.0 +offset_bottom = 52.0 custom_fonts/font = ExtResource( 4 ) text = "Auth Tests" align = 1 @@ -32,22 +32,22 @@ __meta__ = { } [node name="back" parent="." instance=ExtResource( 2 )] -margin_right = 190.354 -margin_bottom = 44.6465 +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 +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__ = { @@ -57,10 +57,10 @@ __meta__ = { [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 +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -69,20 +69,20 @@ __meta__ = { } [node name="Label" type="Label" parent="signup_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 5 ) 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 +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -91,20 +91,20 @@ __meta__ = { } [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 5 ) 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 +offset_left = 10.0 +offset_top = 179.0 +offset_right = 34.0 +offset_bottom = 203.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -113,20 +113,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 5 ) 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 +offset_left = 10.0 +offset_top = 203.0 +offset_right = 34.0 +offset_bottom = 227.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -135,20 +135,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 5 ) 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 +offset_left = 10.0 +offset_top = 227.0 +offset_right = 34.0 +offset_bottom = 251.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -157,20 +157,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 5 ) 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 +offset_left = 10.0 +offset_top = 251.0 +offset_right = 34.0 +offset_bottom = 275.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -179,20 +179,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 5 ) 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 +offset_left = 10.0 +offset_top = 275.0 +offset_right = 34.0 +offset_bottom = 299.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -201,20 +201,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 5 ) 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 +offset_left = 10.0 +offset_top = 299.0 +offset_right = 34.0 +offset_bottom = 323.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -223,10 +223,10 @@ __meta__ = { } [node name="Label" type="Label" parent="delete_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 5 ) text = "Delete" diff --git a/tests/database/database.gd b/tests/database/database.gd index de8bd0b..52f965c 100644 --- a/tests/database/database.gd +++ b/tests/database/database.gd @@ -3,8 +3,8 @@ extends Node2D # Script used for testing the Realtime Database functions of the plugin # 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,114 +14,114 @@ 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 # 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.pressed = false - $back.disabled = true - $test_database.disabled = true + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_database.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_database.disabled = false + _test_running = false + $back.disabled = false + $test_database.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.pressed = true - _test_database() + _print_to_console("Login with email and password has worked") + $login_check.button_pressed = true + _test_database() # 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)) + _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_database_pressed(): - _test_started() - Firebase.Auth.login_with_email_and_password(_email, _password) + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) func _test_database(): - # Print to the console GUI that the test is starting - _print_to_console("STARTING DATABASE TESTS") - - # Get the database reference that we will be working with - _print_to_console("\nGetting the Databse Reference...") - database_reference = Firebase.Database.get_database_reference("FirebaseTester/data", { }) - $get_ref_check.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") - - # Push data to the RTDB - _print_to_console("\nTrying to push data to the RTD...") - database_reference.push({'user_name':'username', 'message':'Hello world!'}) - $push_data_check.pressed = true - yield(get_tree().create_timer(3), "timeout") - - # 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 - - # If nothing has failed to this point, finish the test successfully - _print_to_console("\nFINISHED DATABASE TESTS") - _test_finished() + # Print to the console GUI that the test is starting + _print_to_console("STARTING DATABASE TESTS") + + # Get the database reference that we will be working with + _print_to_console("\nGetting the Databse RefCounted...") + database_reference = Firebase.Database.get_database_reference("FirebaseTester/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",Callable(self,"_on_new_data_update")) # for new data + database_reference.connect("patch_data_update",Callable(self,"_on_patch_data_update")) # for patch data + database_reference.connect("push_failed",Callable(self,"_on_push_failed")) + database_reference.connect("push_successful",Callable(self,"_on_push_successful")) + + # Push data to the RTDB + _print_to_console("\nTrying to push data to the RTD...") + database_reference.push({'user_name':'username', 'message':'Hello world!'}) + $push_data_check.button_pressed = true + await get_tree().create_timer(3).timeout + + # 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.button_pressed = true + + # If nothing has failed to this point, finish the test successfully + _print_to_console("\nFINISHED DATABASE TESTS") + _test_finished() # Function called when new data has been added to the RTDB func _on_new_data_update(new_data : FirebaseResource): - _print_to_console(new_data) - _print_to_console(new_data.key) - _print_to_console(new_data.data) - added_data_key = new_data.key + _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): - _print_to_console(patch_data) - _print_to_console(patch_data.key) - _print_to_console(patch_data.data) - added_data_key = patch_data.key + _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") + _print_to_console_error("Push failed") # Function called when pushing data to the RTDB is successful func _on_push_successful(): - _print_to_console("Push Successful") + _print_to_console("Push Successful") # 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 updated_data = previous_data + data + "\n" - console.bbcode_text = updated_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.bbcode_text - var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_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("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/database/database.tscn b/tests/database/database.tscn index 3afde81..b1537da 100644 --- a/tests/database/database.tscn +++ b/tests/database/database.tscn @@ -1,17 +1,17 @@ [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.png" type="Texture2D" id=2] +[ext_resource path="res://assets/background.png" type="Texture2D" id=3] +[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" 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_disabled.png" type="Texture2D" 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 path="res://fonts/PermanentMarker.ttf" type="FontFile" id=8] +[ext_resource path="res://fonts/PermanentMarker_18.tres" type="FontFile" id=9] -[sub_resource type="DynamicFont" id=1] +[sub_resource type="FontFile" id=1] size = 24 font_data = ExtResource( 8 ) @@ -19,18 +19,18 @@ font_data = ExtResource( 8 ) script = ExtResource( 1 ) [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 +offset_right = 1024.0 +offset_bottom = 600.0 texture = ExtResource( 3 ) __meta__ = { "_edit_use_anchors_": false } [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 13.0 -margin_right = 498.0 -margin_bottom = 52.0 +offset_left = 376.0 +offset_top = 13.0 +offset_right = 498.0 +offset_bottom = 52.0 custom_fonts/font = ExtResource( 4 ) text = "Database Tests" align = 1 @@ -40,16 +40,16 @@ __meta__ = { } [node name="back" parent="." instance=ExtResource( 5 )] -margin_right = 190.354 -margin_bottom = 44.6465 +offset_right = 190.354 +offset_bottom = 44.6465 label = "Back" [node name="test_database" type="TextureButton" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 -margin_top = 65.0 -margin_right = 190.0 -margin_bottom = 110.0 +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 texture_normal = ExtResource( 2 ) texture_disabled = ExtResource( 6 ) script = ExtResource( 7 ) @@ -59,9 +59,9 @@ __meta__ = { label = "Test Database" [node name="label" type="RichTextLabel" parent="test_database"] -margin_left = 10.0 -margin_right = 180.0 -margin_bottom = 45.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 @@ -71,10 +71,10 @@ __meta__ = { } [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__ = { @@ -84,10 +84,10 @@ __meta__ = { [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 +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -96,20 +96,20 @@ __meta__ = { } [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -118,20 +118,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) -text = "Get RTDB Reference" +text = "Get RTDB RefCounted" [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 +offset_left = 10.0 +offset_top = 179.0 +offset_right = 34.0 +offset_bottom = 203.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -140,20 +140,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 203.0 +offset_right = 34.0 +offset_bottom = 227.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -162,10 +162,10 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) text = "Update Data" __meta__ = { diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index c83f6fd..6fa255f 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -3,8 +3,8 @@ extends Node2D # 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 @@ -14,166 +14,166 @@ 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 # 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.pressed = false - $back.disabled = true - $test_firestore.disabled = true + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_firestore.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_firestore.disabled = false + _test_running = false + $back.disabled = false + $test_firestore.disabled = false # Function called if there is an error in the test # Prints the error to the GUI console so the end user can see func _test_error(data) -> void: - _print_to_console_error(data) - _test_finished() + _print_to_console_error(data) + _test_finished() # 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() + _print_to_console("Login with email and password has worked") + $login_check.button_pressed = true + _test_firestore() # 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)) - _test_error("Login Failed") + _print_to_console_error("error code: " + str(error_code)) + _print_to_console_error("message: " + str(message)) + _test_error("Login Failed") # Function called when the end user presses the 'Test Auth' button # Starts the test func _on_test_firestore_pressed(): - _test_started() - Firebase.Auth.login_with_email_and_password(_email, _password) + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) # Main function that is run when testing Firestore func _test_firestore() -> void: - # Print to the console GUI that the test is starting - _print_to_console("\nSTARTING FIRESTORE TESTS") - - # Connect to the test collection - _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") - - # 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") - if(_document == null): - _test_error("Failed to get document") - return - else: - $get_document.pressed = 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") - _print_to_console(_document) - $print_document_2.pressed = true - - # 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 - - # 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") - _print_to_console(result) - $run_query.pressed = true - - # If nothing has failed to this point, finish the test successfully - _print_to_console("\nFINISHED FIRESTORE TESTS") - _test_finished() + # Print to the console GUI that the test is starting + _print_to_console("\nSTARTING FIRESTORE TESTS") + + # Connect to the test collection + _print_to_console("\nConnecting to collection 'Firebasetester'") + _collection = Firebase.Firestore.collection('Firebasetester') + + # Connect to signals needed for testing + _collection.connect("add_document",Callable(self,"on_document_add")) + _collection.connect("get_document",Callable(self,"on_document_get")) + _collection.connect("update_document",Callable(self,"on_document_update")) + _collection.connect("delete_document",Callable(self,"on_document_delete")) + _collection.connect("error",Callable(self,"on_document_error")) + + # Add Document1 to Firestore + _print_to_console("Trying to add a document") + var add_task : FirestoreTask = _collection.add("Document1", {'name': 'Document1', 'active': 'true'}) + _document = await add_task.add_document + $add_document.button_pressed = true + + # Get Document1 (Document that has been added from the previous step) + _print_to_console("Trying to get 'Document1") + _collection.get('Document1') + _document = await _collection.get_document + if(_document == null): + _test_error("Failed to get document") + return + else: + $get_document.button_pressed = true + + # Print Document1 to the console GUI + _print_to_console("Trying to print contents of Document1") + _print_to_console(_document) + $print_document.button_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 = await up_task.update_document + $update_document.button_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 = await _collection.get_document + $get_document_2.button_pressed = true + + # Print Document1 to the console GUI + _print_to_console("Trying to print contents of Document1") + _print_to_console(_document) + $print_document_2.button_pressed = true + + # Delete Document1 from Firestore + _print_to_console("Trying to delete Doucment1") + var del_task : FirestoreTask = _collection.delete("Document1") + _document = await del_task.delete_document + $delete_document.button_pressed = true + + # 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 = await query_task.task_finished + _print_to_console(result) + $run_query.button_pressed = true + + # 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") + _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") + _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") + _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") + _print_to_console("Document deleted successfully") # 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") + _print_to_console_error("error code: " + str(code)) + _print_to_console_error("message: " + str(message)) + _test_error("There was an issue") # 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 updated_data = previous_data + data + "\n" - console.bbcode_text = updated_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.bbcode_text - var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_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("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/firestore/firestore.tscn b/tests/firestore/firestore.tscn index 01775cc..ba5c392 100644 --- a/tests/firestore/firestore.tscn +++ b/tests/firestore/firestore.tscn @@ -1,17 +1,17 @@ [gd_scene load_steps=11 format=2] [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.png" type="Texture2D" id=2] +[ext_resource path="res://assets/background.png" type="Texture2D" id=3] +[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" 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_disabled.png" type="Texture2D" 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 path="res://fonts/PermanentMarker.ttf" type="FontFile" id=8] +[ext_resource path="res://fonts/PermanentMarker_18.tres" type="FontFile" id=9] -[sub_resource type="DynamicFont" id=1] +[sub_resource type="FontFile" id=1] size = 23 font_data = ExtResource( 8 ) @@ -19,20 +19,20 @@ font_data = ExtResource( 8 ) 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 +offset_left = 5.59372 +offset_top = 2.00226 +offset_right = 1029.59 +offset_bottom = 602.002 texture = ExtResource( 3 ) __meta__ = { "_edit_use_anchors_": false } [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 11.8108 -margin_right = 557.0 -margin_bottom = 55.8108 +offset_left = 376.0 +offset_top = 11.8108 +offset_right = 557.0 +offset_bottom = 55.8108 custom_fonts/font = ExtResource( 4 ) text = "Firestore Tests" align = 1 @@ -42,16 +42,16 @@ __meta__ = { } [node name="back" parent="." instance=ExtResource( 5 )] -margin_right = 190.354 -margin_bottom = 44.6465 +offset_right = 190.354 +offset_bottom = 44.6465 label = "Back" [node name="test_firestore" type="TextureButton" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 -margin_top = 65.0 -margin_right = 190.0 -margin_bottom = 110.0 +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 texture_normal = ExtResource( 2 ) texture_disabled = ExtResource( 6 ) script = ExtResource( 7 ) @@ -61,9 +61,9 @@ __meta__ = { label = "Test Firestore" [node name="label" type="RichTextLabel" parent="test_firestore"] -margin_left = 10.0 -margin_right = 180.0 -margin_bottom = 45.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 @@ -73,10 +73,10 @@ __meta__ = { } [node name="console" type="RichTextLabel" parent="."] -margin_left = 249.251 -margin_top = 117.012 -margin_right = 945.251 -margin_bottom = 522.012 +offset_left = 249.251 +offset_top = 117.012 +offset_right = 945.251 +offset_bottom = 522.012 bbcode_enabled = true scroll_following = true __meta__ = { @@ -86,10 +86,10 @@ __meta__ = { [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 +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -98,20 +98,20 @@ __meta__ = { } [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -120,10 +120,10 @@ __meta__ = { } [node name="Label" type="Label" parent="add_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) text = "Add Document" __meta__ = { @@ -133,10 +133,10 @@ __meta__ = { [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 +offset_left = 10.0 +offset_top = 180.0 +offset_right = 34.0 +offset_bottom = 204.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -145,20 +145,20 @@ __meta__ = { } [node name="Label" type="Label" parent="get_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 203.0 +offset_right = 34.0 +offset_bottom = 227.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -167,20 +167,20 @@ __meta__ = { } [node name="Label" type="Label" parent="print_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 224.0 +offset_right = 34.0 +offset_bottom = 248.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -189,20 +189,20 @@ __meta__ = { } [node name="Label" type="Label" parent="update_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 246.0 +offset_right = 34.0 +offset_bottom = 270.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -211,20 +211,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 266.0 +offset_right = 34.0 +offset_bottom = 290.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -233,20 +233,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 285.0 +offset_right = 34.0 +offset_bottom = 309.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -255,20 +255,20 @@ __meta__ = { } [node name="Label" type="Label" parent="delete_document"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 309.0 +offset_right = 34.0 +offset_bottom = 333.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -277,10 +277,10 @@ __meta__ = { } [node name="Label" type="Label" parent="run_query"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) text = "Run Query" diff --git a/tests/links/links.gd b/tests/links/links.gd index 54d0574..075d549 100644 --- a/tests/links/links.gd +++ b/tests/links/links.gd @@ -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,97 +15,97 @@ 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 # 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.pressed = false - $back.disabled = true - $test_links.disabled = true + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_links.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_links.disabled = false + _test_running = false + $back.disabled = false + $test_links.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.pressed = true - _test_links() + _print_to_console("Login with email and password has worked") + $login_check.button_pressed = true + _test_links() # 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)) - _test_error("Login Failed") + _print_to_console_error("error code: " + str(error_code)) + _print_to_console_error("message: " + str(message)) + _test_error("Login Failed") # Function called if there is an error in the test # Prints the error to the GUI console so the end user can see func _test_error(data) -> void: - _print_to_console_error(data) - _test_finished() + _print_to_console_error(data) + _test_finished() # Function called when the end user presses the 'Test Links' button # Starts the test func _on_test_links_pressed(): - _test_started() - Firebase.Auth.login_with_email_and_password(_email, _password) + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) # Main function that is run when testing Dynamic Links func _test_links(): - # Print to the console GUI that the test is starting - _print_to_console("STARTING LINKS TESTS") - - # Connect to signals needed for testing - Firebase.DynamicLinks.connect("dynamic_link_generated", 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 - - # 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 - - # If nothing has failed to this point, finish the test successfully - _print_to_console("\nFINISHED LINKS TESTS") - _test_finished() + # Print to the console GUI that the test is starting + _print_to_console("STARTING LINKS TESTS") + + # Connect to signals needed for testing + 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) + 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) + 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") + _test_finished() # Function used to print the link data to the console GUI func print_link(link_data): - _print_to_console(link_data) - emit_signal('link_printed') + _print_to_console(link_data) + emit_signal('link_printed') # 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 updated_data = previous_data + data + "\n" - console.bbcode_text = updated_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.bbcode_text - var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_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("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/links/links.tscn b/tests/links/links.tscn index c7d6af5..cbd795a 100644 --- a/tests/links/links.tscn +++ b/tests/links/links.tscn @@ -1,9 +1,9 @@ [gd_scene load_steps=6 format=2] [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://fonts/PermanentMarker_18.tres" type="FontFile" id=2] +[ext_resource path="res://assets/background.png" type="Texture2D" id=3] +[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" id=4] [ext_resource path="res://assets/buttons/normal_button.tscn" type="PackedScene" id=5] @@ -11,18 +11,18 @@ script = ExtResource( 1 ) [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 +offset_right = 1024.0 +offset_bottom = 600.0 texture = ExtResource( 3 ) __meta__ = { "_edit_use_anchors_": false } [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 13.0 -margin_right = 498.0 -margin_bottom = 52.0 +offset_left = 376.0 +offset_top = 13.0 +offset_right = 498.0 +offset_bottom = 52.0 custom_fonts/font = ExtResource( 4 ) text = "Links Tests" align = 1 @@ -32,21 +32,21 @@ __meta__ = { } [node name="back" parent="." instance=ExtResource( 5 )] -margin_right = 190.354 -margin_bottom = 44.6465 +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 +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 +offset_left = 250.586 +offset_top = 117.012 +offset_right = 946.586 +offset_bottom = 522.012 bbcode_enabled = true scroll_following = true __meta__ = { @@ -56,10 +56,10 @@ __meta__ = { [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 +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -68,20 +68,20 @@ __meta__ = { } [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 2 ) 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 +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -90,20 +90,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 2 ) 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 +offset_left = 10.0 +offset_top = 179.0 +offset_right = 34.0 +offset_bottom = 203.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -112,10 +112,10 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 2 ) text = "Guessable Link" diff --git a/tests/storage/storage.gd b/tests/storage/storage.gd index 96717dd..416d1c2 100644 --- a/tests/storage/storage.gd +++ b/tests/storage/storage.gd @@ -3,8 +3,8 @@ extends Node2D # 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,195 +12,195 @@ 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 # 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.pressed = false - $back.disabled = true - $test_storage.disabled = true - $image.texture = null + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_storage.disabled = true + $image.texture = null # 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_storage.disabled = false + _test_running = false + $back.disabled = false + $test_storage.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.pressed = true - _test_storage() + _print_to_console("Login with email and password has worked") + $login_check.button_pressed = true + _test_storage() # 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)) + _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 Storage' button # Starts the test func _on_test_storage_pressed(): - _test_started() - Firebase.Auth.login_with_email_and_password(_email, _password) + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) # Main function that is run when testing Storage func _test_storage(): - # Print to the console GUI that the test is starting - _print_to_console("STARTING STORAGE TESTS") - - # 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 - - # 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) - $image.texture = converted_image - $download_image_check.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 - - # 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 - - # 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") - _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 - - # 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 - - # 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 - - # 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") - _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 - - # 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 - - # 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 - - # 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 - - # 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") - _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 - - # If nothing has failed to this point, finish the test successfully - _print_to_console("\nFINISHED STORAGE TESTS") - _test_finished() + # Print to the console GUI that the test is starting + _print_to_console("STARTING STORAGE TESTS") + + # 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") + await upload_task.task_finished + $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') + await image.task_finished + var converted_image = task2image(image) + $image.texture = converted_image + $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() + await url_task.task_finished + _print_to_console(url_task.data) + $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() + await meta_task.task_finished + _print_to_console(meta_task.data) + $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() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + var delete_task = Firebase.Storage.ref("Firebasetester/upload/image.png").delete() + await delete_task.task_finished + _print_to_console("After Delete...") + list_all_task = Firebase.Storage.ref("Firebasetester").list_all() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + $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") + await upload_task.task_finished + $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() + await meta_task.task_finished + _print_to_console(meta_task.data) + $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() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + delete_task = Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").delete() + await delete_task.task_finished + _print_to_console("After Delete...") + list_all_task = Firebase.Storage.ref("Firebasetester").list_all() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + $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", {}) + await upload_task.task_finished + $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"}) + await meta_task.task_finished + $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() + await meta_task.task_finished + _print_to_console(meta_task.data) + $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() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + delete_task = Firebase.Storage.ref("Firebasetester/upload/junkdata").delete() + await delete_task.task_finished + _print_to_console("After Delete...") + list_all_task = Firebase.Storage.ref("Firebasetester").list_all() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + $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() # 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 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: - var new_image := Image.new() - match typeof(task.data): - TYPE_RAW_ARRAY: - var data : PoolByteArray = task.data - if data.size()>1: - match data.subarray(0,1).hex_encode(): - "ffd8": - new_image.load_jpg_from_buffer(data) - "8950": - new_image.load_png_from_buffer(data) - TYPE_DICTIONARY: - _print_to_console_error("ERROR %s: could not find image requested" % task.data.error.code) - var new_texture := ImageTexture.new() - new_texture.create_from_image(new_image) - return new_texture + var new_image := Image.new() + match typeof(task.data): + TYPE_PACKED_BYTE_ARRAY: + var data : PackedByteArray = task.data + if data.size()>1: + match data.slice(0,1).hex_encode(): + "ffd8": + new_image.load_jpg_from_buffer(data) + "8950": + new_image.load_png_from_buffer(data) + TYPE_DICTIONARY: + _print_to_console_error("ERROR %s: could not find image requested" % task.data.error.code) + var new_texture := ImageTexture.new() + new_texture.create_from_image(new_image) + return new_texture # 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 updated_data = previous_data + data + "\n" - console.bbcode_text = updated_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.bbcode_text - var updated_data = previous_data + "[color=red]" + data + "[/color] \n" - console.bbcode_text = updated_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("res://main.tscn") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/storage/storage.tscn b/tests/storage/storage.tscn index 3dc6d67..fa4f0f9 100644 --- a/tests/storage/storage.tscn +++ b/tests/storage/storage.tscn @@ -1,17 +1,17 @@ [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.png" type="Texture2D" id=2] +[ext_resource path="res://assets/background.png" type="Texture2D" id=3] +[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" 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_disabled.png" type="Texture2D" 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 path="res://fonts/PermanentMarker.ttf" type="FontFile" id=8] +[ext_resource path="res://fonts/PermanentMarker_18.tres" type="FontFile" id=9] -[sub_resource type="DynamicFont" id=1] +[sub_resource type="FontFile" id=1] size = 26 font_data = ExtResource( 8 ) @@ -19,18 +19,18 @@ font_data = ExtResource( 8 ) script = ExtResource( 1 ) [node name="background" type="TextureRect" parent="."] -margin_right = 1024.0 -margin_bottom = 600.0 +offset_right = 1024.0 +offset_bottom = 600.0 texture = ExtResource( 3 ) __meta__ = { "_edit_use_anchors_": false } [node name="title" type="Label" parent="."] -margin_left = 376.0 -margin_top = 12.0 -margin_right = 557.0 -margin_bottom = 56.0 +offset_left = 376.0 +offset_top = 12.0 +offset_right = 557.0 +offset_bottom = 56.0 custom_fonts/font = ExtResource( 4 ) text = "Storage Tests" align = 1 @@ -40,16 +40,16 @@ __meta__ = { } [node name="back" parent="." instance=ExtResource( 5 )] -margin_right = 190.354 -margin_bottom = 44.6465 +offset_right = 190.354 +offset_bottom = 44.6465 label = "Back" [node name="test_storage" type="TextureButton" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 -margin_top = 65.0 -margin_right = 190.0 -margin_bottom = 110.0 +offset_top = 65.0 +offset_right = 190.0 +offset_bottom = 110.0 texture_normal = ExtResource( 2 ) texture_disabled = ExtResource( 6 ) script = ExtResource( 7 ) @@ -59,9 +59,9 @@ __meta__ = { label = "Test Storage" [node name="label" type="RichTextLabel" parent="test_storage"] -margin_left = 10.0 -margin_right = 180.0 -margin_bottom = 45.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 @@ -71,19 +71,19 @@ __meta__ = { } [node name="image" type="TextureRect" parent="."] -margin_left = 40.0 -margin_top = 450.0 -margin_right = 104.0 -margin_bottom = 514.0 +offset_left = 40.0 +offset_top = 450.0 +offset_right = 104.0 +offset_bottom = 514.0 __meta__ = { "_edit_use_anchors_": false } [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__ = { @@ -93,10 +93,10 @@ __meta__ = { [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 +offset_left = 10.0 +offset_top = 131.0 +offset_right = 34.0 +offset_bottom = 155.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -105,20 +105,20 @@ __meta__ = { } [node name="Label" type="Label" parent="login_check"] -margin_left = 26.0596 -margin_top = -3.06488 -margin_right = 76.0596 -margin_bottom = 22.9351 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 155.0 +offset_right = 34.0 +offset_bottom = 179.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -127,20 +127,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 179.0 +offset_right = 34.0 +offset_bottom = 203.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -149,20 +149,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 203.0 +offset_right = 34.0 +offset_bottom = 227.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -171,20 +171,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 227.0 +offset_right = 34.0 +offset_bottom = 251.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -193,20 +193,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 251.0 +offset_right = 34.0 +offset_bottom = 275.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -215,20 +215,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 275.0 +offset_right = 34.0 +offset_bottom = 299.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -237,20 +237,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 299.0 +offset_right = 34.0 +offset_bottom = 323.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -259,20 +259,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 323.0 +offset_right = 34.0 +offset_bottom = 347.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -281,20 +281,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 347.0 +offset_right = 34.0 +offset_bottom = 371.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -303,20 +303,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 371.0 +offset_right = 34.0 +offset_bottom = 395.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -325,20 +325,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 395.0 +offset_right = 34.0 +offset_bottom = 419.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -347,20 +347,20 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) 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 +offset_left = 10.0 +offset_top = 419.0 +offset_right = 34.0 +offset_bottom = 443.0 mouse_filter = 2 disabled = true button_mask = 0 @@ -369,10 +369,10 @@ __meta__ = { } [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 +offset_left = 26.0596 +offset_top = -3.06488 +offset_right = 76.0596 +offset_bottom = 22.9351 custom_fonts/font = ExtResource( 9 ) text = "Delete String" From dacecf88b2321e59c1cb257993b117a68a6cd168 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Mon, 6 Feb 2023 09:08:53 -0500 Subject: [PATCH 03/22] Fix remaining 4.x issues --- addons/godot-firebase/Utilies.gd | 2 + addons/godot-firebase/auth/auth.gd | 3 +- addons/godot-firebase/database/database.gd | 1 + addons/godot-firebase/firebase/firebase.tscn | 33 +++---- addons/godot-firebase/storage/storage.gd | 4 +- .../storage/storage_reference.gd | 1 - addons/http-sse-client/HTTPSSEClient.gd | 13 +-- tests/auth/auth.gd | 20 ++-- tests/database/database.gd | 15 ++- tests/database/database.tscn | 95 +++++-------------- tests/firestore/firestore.gd | 14 ++- tests/storage/storage.gd | 10 +- 12 files changed, 91 insertions(+), 120 deletions(-) diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd index 1e66a0c..6e821bc 100644 --- a/addons/godot-firebase/Utilies.gd +++ b/addons/godot-firebase/Utilies.gd @@ -2,6 +2,8 @@ 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: diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 55dd786..11694f8 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -403,7 +403,7 @@ func manual_token_refresh(auth_data): # 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 : PackedStringArray, body : PackedByteArray) -> void: - var json = Utilities.get_json_data(body) + var json = Utilities.get_json_data(body.get_string_from_utf8()) is_busy = false var res if response_code == 0: @@ -467,7 +467,6 @@ func save_auth(auth : Dictionary) -> void: Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error())) else: encrypted_file.store_line(JSON.stringify(auth)) - encrypted_file.close() # Function used to load the auth data file that has been stored locally diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index 430b33a..4d928cb 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -42,6 +42,7 @@ func _on_FirebaseAuth_logout() -> void: func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() var pusher : HTTPRequest = HTTPRequest.new() + pusher.use_threads = true var listener : Node = Node.new() listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) var store : FirebaseDatabaseStore = FirebaseDatabaseStore.new() diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index 3bc100d..d5b34d7 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -1,31 +1,32 @@ -[gd_scene load_steps=8 format=2] +[gd_scene load_steps=8 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"] [node name="Firebase" type="Node"] -process_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") diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 3fcf62b..1b4b93e 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -192,7 +192,7 @@ func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Sto var data = await info_task.task_finished if info_task.result == OK: - task._url += data.downloadTokens # I don't see how this will ever work, but who knows; pretty sure it doesn't, which means in theory it should be running the else every single time; data is a PackedByteArray, not sure what the original code was smoking + task._url += info_task.data.downloadTokens else: task.data = info_task.data task.response_headers = info_task.response_headers @@ -306,7 +306,7 @@ func _finish_request(result : int) -> void: next_task = _pending_tasks.pop_front() task.finished = true - task.task_finished.emit() + 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) else: diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 08b1a49..3cad552 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -104,7 +104,6 @@ func put_string(data : String, metadata := {}) -> StorageTask: func put_file(file_path : String, metadata := {}) -> StorageTask: var file := FileAccess.open(file_path, FileAccess.READ) var data := file.get_buffer(file.get_length()) - file.close() if "Content-Type" in metadata: metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) diff --git a/addons/http-sse-client/HTTPSSEClient.gd b/addons/http-sse-client/HTTPSSEClient.gd index aac13bd..2a01706 100644 --- a/addons/http-sse-client/HTTPSSEClient.gd +++ b/addons/http-sse-client/HTTPSSEClient.gd @@ -23,6 +23,7 @@ var is_requested = false var response_body = PackedByteArray() 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 @@ -103,7 +104,7 @@ func _process(delta): if response_body.size() > 0: _parse_response_body(headers) -func get_event_data(body : String) -> Dictionary: +func get_event_data(body : String): var result = {} var event_idx = body.find(event_tag) if event_idx == -1: @@ -113,11 +114,11 @@ func get_event_data(body : String) -> Dictionary: 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() + var event_value = event.replace(event_tag, "").strip_edges() + assert(event_value) + assert(event_value.length() > 0) + result["event"] = event_value + var data = body.right(body.length() - (data_idx + data_tag.length())).strip_edges() assert(data) assert(data.length() > 0) result["data"] = data diff --git a/tests/auth/auth.gd b/tests/auth/auth.gd index d17c581..ce62548 100644 --- a/tests/auth/auth.gd +++ b/tests/auth/auth.gd @@ -55,27 +55,27 @@ func _on_test_auth_pressed() -> void: await get_tree().create_timer(_timer_length).timeout # Start Login test using the first email - if not Firebase.Auth.auth.is_empty(): + if _auth_error != true: $signup_check.button_pressed = true _print_to_console("\nTrying to login...") Firebase.Auth.login_with_email_and_password(_email1, _password1) await get_tree().create_timer(_timer_length).timeout # Check Auth File - if not Firebase.Auth.auth.is_empty(): + if _auth_error != true: $login_check.button_pressed = true _check_auth_file() await get_tree().create_timer(_timer_length).timeout # Get User Data Test - if not Firebase.Auth.auth.is_empty(): + if _auth_error != true: $auth_file_check.button_pressed = true _print_to_console("\nTrying to get user data...") Firebase.Auth.get_user_data() await get_tree().create_timer(_timer_length).timeout # Change User Password - if not Firebase.Auth.auth.is_empty(): + if _auth_error != true: $user_data_check.button_pressed = true _print_to_console("\nTrying to change user password...") Firebase.Auth.change_user_password(_password2) @@ -86,7 +86,7 @@ func _on_test_auth_pressed() -> void: await get_tree().create_timer(_timer_length).timeout # Change the user email from the first email to the second email - if not Firebase.Auth.auth.is_empty(): + if _auth_error != true: $change_pass_check.button_pressed = true _print_to_console("\nTrying to change user email...") Firebase.Auth.change_user_email(_email2) @@ -97,20 +97,20 @@ func _on_test_auth_pressed() -> void: await get_tree().create_timer(_timer_length).timeout # Login with the new credentials - if not Firebase.Auth.auth.is_empty(): + if _auth_error != 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) await get_tree().create_timer(_timer_length).timeout # Start Delete Account test - if not Firebase.Auth.auth.is_empty(): + if _auth_error != 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 not Firebase.Auth.auth.is_empty(): + if _auth_error != true: $delete_check.button_pressed = true _print_to_console("\nFINISHED AUTH TESTS") _test_finished() @@ -118,7 +118,7 @@ func _on_test_auth_pressed() -> void: _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() @@ -130,7 +130,7 @@ func _on_FirebaseAuth_signup_succeeded(_auth) -> void: _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: diff --git a/tests/database/database.gd b/tests/database/database.gd index 52f965c..51f2d04 100644 --- a/tests/database/database.gd +++ b/tests/database/database.gd @@ -1,6 +1,11 @@ extends Node2D # 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 @@ -73,7 +78,7 @@ func _test_database(): _print_to_console("\nTrying to push data to the RTD...") database_reference.push({'user_name':'username', 'message':'Hello world!'}) $push_data_check.button_pressed = true - await get_tree().create_timer(3).timeout + await data_key_ready # Update data in the RTDB _print_to_console("\nTrying to update the DB") @@ -86,24 +91,28 @@ 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 used to print data to the console GUI for the end user diff --git a/tests/database/database.tscn b/tests/database/database.tscn index b1537da..a062ada 100644 --- a/tests/database/database.tscn +++ b/tests/database/database.tscn @@ -1,74 +1,53 @@ -[gd_scene load_steps=11 format=2] +[gd_scene load_steps=7 format=3 uid="uid://c34qiepv6pvbq"] -[ext_resource path="res://tests/database/database.gd" type="Script" id=1] -[ext_resource path="res://assets/buttons/normal_button.png" type="Texture2D" id=2] -[ext_resource path="res://assets/background.png" type="Texture2D" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" 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="Texture2D" id=6] -[ext_resource path="res://assets/buttons/normal_button.gd" type="Script" id=7] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="FontFile" id=8] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="FontFile" id=9] - - -[sub_resource type="FontFile" id=1] -size = 24 -font_data = ExtResource( 8 ) +[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" 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="Node2D"] -script = ExtResource( 1 ) +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] offset_right = 1024.0 offset_bottom = 600.0 -texture = ExtResource( 3 ) -__meta__ = { -"_edit_use_anchors_": false -} +texture = ExtResource("3") [node name="title" type="Label" parent="."] offset_left = 376.0 offset_top = 13.0 offset_right = 498.0 offset_bottom = 52.0 -custom_fonts/font = ExtResource( 4 ) text = "Database Tests" -align = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 5 )] +[node name="back" parent="." instance=ExtResource("5")] +anchors_preset = 15 offset_right = 190.354 offset_bottom = 44.6465 label = "Back" [node name="test_database" type="TextureButton" parent="."] +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 offset_top = 65.0 offset_right = 190.0 offset_bottom = 110.0 -texture_normal = ExtResource( 2 ) -texture_disabled = ExtResource( 6 ) -script = ExtResource( 7 ) -__meta__ = { -"_edit_use_anchors_": false -} +texture_normal = ExtResource("2") +texture_disabled = ExtResource("6") +script = ExtResource("7") label = "Test Database" [node name="label" type="RichTextLabel" parent="test_database"] +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="."] offset_left = 250.586 @@ -77,13 +56,8 @@ 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", -]] +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 131.0 offset_right = 34.0 @@ -91,21 +65,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Login" -[node name="get_ref_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="get_ref_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 155.0 offset_right = 34.0 @@ -113,21 +82,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Get RTDB RefCounted" -[node name="push_data_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="push_data_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 179.0 offset_right = 34.0 @@ -135,21 +99,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Push Data" -[node name="update_data_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="update_data_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 203.0 offset_right = 34.0 @@ -157,20 +116,14 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Update Data" -__meta__ = { -"_edit_use_anchors_": false -} [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/firestore/firestore.gd b/tests/firestore/firestore.gd index 6fa255f..e6d7677 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -41,6 +41,10 @@ func _test_error(data) -> void: _print_to_console_error(data) _test_finished() +func _cleanup_previous_run(): + var del_task : FirestoreTask = _collection.delete("Document1") + _document = await del_task.delete_document + # 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") @@ -75,7 +79,7 @@ func _test_firestore() -> void: _collection.connect("update_document",Callable(self,"on_document_update")) _collection.connect("delete_document",Callable(self,"on_document_delete")) _collection.connect("error",Callable(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'}) @@ -83,8 +87,8 @@ func _test_firestore() -> void: $add_document.button_pressed = true # Get Document1 (Document that has been added from the previous step) - _print_to_console("Trying to get 'Document1") - _collection.get('Document1') + _print_to_console("Trying to get Document1") + var get_task = _collection.get_doc('Document1') _document = await _collection.get_document if(_document == null): _test_error("Failed to get document") @@ -104,8 +108,8 @@ func _test_firestore() -> void: $update_document.button_pressed = true # Get Document1 (With updated that has been added from the previous step) - _print_to_console("Trying to get 'Document1") - _collection.get('Document1') + _print_to_console("Trying to get Document1") + _collection.get_doc('Document1') _document = await _collection.get_document $get_document_2.button_pressed = true diff --git a/tests/storage/storage.gd b/tests/storage/storage.gd index 416d1c2..c8e3ed0 100644 --- a/tests/storage/storage.gd +++ b/tests/storage/storage.gd @@ -66,7 +66,7 @@ func _test_storage(): # 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') - await image.task_finished + var data = await image.task_finished var converted_image = task2image(image) $image.texture = converted_image $download_image_check.button_pressed = true @@ -174,10 +174,12 @@ func task2image(task : StorageTask) -> ImageTexture: TYPE_PACKED_BYTE_ARRAY: var data : PackedByteArray = task.data if data.size()>1: - match data.slice(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) From 37b0f9d6ba392dfa7287e95fff1a51a0804e2e87 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Fri, 8 Dec 2023 11:36:57 -0500 Subject: [PATCH 04/22] Update for once and delete --- addons/godot-firebase/database/database.gd | 3 ++ addons/godot-firebase/database/reference.gd | 39 +++++++++++++++++++-- project.godot | 2 +- tests/database/database.gd | 33 ++++++++++++++--- tests/database/database.tscn | 35 ++++++++++++++++++ 5 files changed, 104 insertions(+), 8 deletions(-) diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index 4d928cb..a2bf082 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -41,6 +41,8 @@ func _on_FirebaseAuth_logout() -> void: func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() + var getter := HTTPRequest.new() + getter.use_threads = true var pusher : HTTPRequest = HTTPRequest.new() pusher.use_threads = true var listener : Node = Node.new() @@ -49,6 +51,7 @@ func get_database_reference(path : String, filter : Dictionary = {}) -> Firebase firebase_reference.set_db_path(path, filter) firebase_reference.set_auth_and_config(_auth, _config) firebase_reference.set_pusher(pusher) + firebase_reference.set_getter(getter) firebase_reference.set_listener(listener) firebase_reference.set_store(store) add_child(firebase_reference) diff --git a/addons/godot-firebase/database/reference.gd b/addons/godot-firebase/database/reference.gd index 0c485cd..803da37 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -1,5 +1,5 @@ -## @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 @@ -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" @@ -21,6 +25,7 @@ const END_AT : String = "endAt" const EQUAL_TO : String = "equalTo" var _pusher : HTTPRequest +var _getter : HTTPRequest var _listener : Node var _store : FirebaseDatabaseStore var _auth : Dictionary @@ -29,6 +34,7 @@ var _filter_query : Dictionary var _db_path : String var _cached_filter : String var _push_queue : Array = [] +var _get_queue : Array = [] var _update_queue : Array = [] var _delete_queue : Array = [] var _can_connect_to_host : bool = false @@ -64,6 +70,13 @@ func set_pusher(pusher_ref : HTTPRequest) -> void: add_child(_pusher) _pusher.request_completed.connect(on_push_request_complete) +func set_getter(getter_ref : HTTPRequest) -> void: + if !_getter: + _getter = getter_ref + add_child(_getter) + _getter.request_completed.connect(on_get_request_complete) + + func set_listener(listener_ref : Node) -> void: if !_listener: _listener = listener_ref @@ -126,6 +139,16 @@ func delete(reference : String) -> void: else: _delete_queue.append(reference) +# +# Gets a data snapshot once at the position passed in +# +func once(reference : String) -> void: + if _getter.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: + var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() + _getter.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") + else: + _get_queue.append(reference) + # # Returns a deep copy of the current local copy of the data stored at this reference in the Firebase # Realtime Database. @@ -204,3 +227,15 @@ func on_push_request_complete(result : int, response_code : int, headers : Packe if _delete_queue.size() > 0: delete(_delete_queue.pop_front()) + +func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: + if response_code == HTTPClient.RESPONSE_OK: + var bod = Utilities.get_json_data(body) + once_successful.emit(bod) + else: + once_failed.emit() + + # handle queued operations + if _get_queue.size() > 0: + once(_get_queue.pop_front()) + diff --git a/project.godot b/project.godot index 6b5a7ac..2ef49f0 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="FirebaseTestHarness" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.0") +config/features=PackedStringArray("4.2") config/icon="res://icon.png" [autoload] diff --git a/tests/database/database.gd b/tests/database/database.gd index 51f2d04..e67cc49 100644 --- a/tests/database/database.gd +++ b/tests/database/database.gd @@ -69,10 +69,12 @@ func _test_database(): # Connect to signals needed for testing _print_to_console("\nConnecting signals for the RTD...") - database_reference.connect("new_data_update",Callable(self,"_on_new_data_update")) # for new data - database_reference.connect("patch_data_update",Callable(self,"_on_patch_data_update")) # for patch data - database_reference.connect("push_failed",Callable(self,"_on_push_failed")) - database_reference.connect("push_successful",Callable(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) + database_reference.connect("once_failed", _on_once_failed) + database_reference.connect("once_successful", _on_once_successful) # Push data to the RTDB _print_to_console("\nTrying to push data to the RTD...") @@ -80,11 +82,21 @@ func _test_database(): $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") + database_reference.once(added_data_key + "/user_name") + $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.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") _test_finished() @@ -114,6 +126,17 @@ func _on_push_failed(): func _on_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): diff --git a/tests/database/database.tscn b/tests/database/database.tscn index a062ada..070a5f6 100644 --- a/tests/database/database.tscn +++ b/tests/database/database.tscn @@ -125,5 +125,40 @@ offset_right = 76.0596 offset_bottom = 22.9351 text = "Update Data" +[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"] From ca1abce272c2470db470faf09393fc44757b6c29 Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 17 Jan 2024 19:14:19 -0500 Subject: [PATCH 05/22] Add 'Web' to OS.get_name() for HTML and UWP exports --- addons/godot-firebase/firestore/firestore.gd | 2 +- addons/godot-firebase/functions/functions.gd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 4fe44d6..545c58d 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -68,7 +68,7 @@ var auth : 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 OS.get_name() in ["HTML5", "UWP", "Web"] else OS.get_unique_id() var _base_url : String = "" diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index e7f2b5d..b769daf 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -42,7 +42,7 @@ 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 OS.get_name() in ["HTML5", "UWP", "Web"] else OS.get_unique_id() var _base_url : String = "" From b42667b7b5b40c8b650b4097ddcd878475d1cd4b Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sun, 21 Jan 2024 06:20:15 -0500 Subject: [PATCH 06/22] Some debugging stuff --- addons/godot-firebase/Utilies.gd | 11 +- addons/godot-firebase/auth/auth.gd | 4 +- .../godot-firebase/auth/providers/facebook.gd | 2 +- addons/godot-firebase/firebase/firebase.gd | 6 +- addons/godot-firebase/firebase/firebase.tscn | 1 + addons/godot-firebase/firestore/firestore.gd | 2 +- addons/godot-firebase/functions/functions.gd | 2 +- .../storage/storage_reference.gd | 2 +- assets/buttons/normal_button.tscn | 25 ++--- main.tscn | 2 +- project.godot | 4 +- tests/auth/auth.tscn | 105 +++++------------- tests/database/database.tscn | 2 +- 13 files changed, 61 insertions(+), 107 deletions(-) diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd index 6e821bc..ff4e09a 100644 --- a/addons/godot-firebase/Utilies.gd +++ b/addons/godot-firebase/Utilies.gd @@ -2,11 +2,20 @@ extends Node class_name Utilities static func get_json_data(value): + Firebase._print(value) if value is PackedByteArray: value = value.get_string_from_utf8() var json = JSON.new() var json_parse_result = json.parse(value) + Firebase._print(json_parse_result) if json_parse_result == OK: + Firebase._print(json.data) return json.data + else: + json["data"]["error"] = json_parse_result + Firebase._print(json) - return null + return json + +static func is_web() -> bool: + return OS.get_name() in ["HTML5", "Web"] diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 11694f8..7f72461 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -147,7 +147,7 @@ func _ready() -> void: tcp_timer.wait_time = tcp_timeout tcp_timer.timeout.connect(_tcp_stream_timer) - if OS.get_name() == "HTML5": + if Utilities.is_web(): _local_uri += "tmp_js_export.html" @@ -280,7 +280,7 @@ func get_auth_with_redirect(provider: AuthProvider) -> void: 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"): + 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 diff --git a/addons/godot-firebase/auth/providers/facebook.gd b/addons/godot-firebase/auth/providers/facebook.gd index 9dc4b1d..926c4a1 100644 --- a/addons/godot-firebase/auth/providers/facebook.gd +++ b/addons/godot-firebase/auth/providers/facebook.gd @@ -11,7 +11,7 @@ func _init(client_id: String,client_secret: String): self.provider_id = "facebook.com" self.params.scope = "public_profile" self.params.state = str(randf_range(0, 1)) - if OS.get_name() == "HTML5": + if Utilities.is_web(): self.should_exchange = false self.params.response_type = "token" else: diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index 8c106b3..444d550 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -57,7 +57,7 @@ var _config : Dictionary = { "clientSecret" : "", "domainUriPrefix" : "", "functionsGeoZone" : "", - "cacheLocation":"user://.firebase_cache", + "cacheLocation":"", "emulators": { "ports" : { "authentication" : "", @@ -136,5 +136,5 @@ func _setup_modules() -> void: func _printerr(error : String) -> void: printerr("[Firebase Error] >> "+error) -func _print(msg : String) -> void: - print("[Firebase] >> "+msg) +func _print(msg) -> void: + print("[Firebase] >> " + str(msg)) diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index d5b34d7..cd922a6 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -12,6 +12,7 @@ script = ExtResource("3") [node name="Auth" type="HTTPRequest" parent="."] +accept_gzip = false max_redirects = 12 timeout = 10.0 script = ExtResource("4") diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 545c58d..5586777 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -68,7 +68,7 @@ var auth : Dictionary var _config : Dictionary = {} var _cache_loc: String -var _encrypt_key := "5vg76n90345f7w390346" if OS.get_name() in ["HTML5", "UWP", "Web"] else OS.get_unique_id() +var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_unique_id() var _base_url : String = "" diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index b769daf..365ae83 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -42,7 +42,7 @@ var auth : Dictionary var _config : Dictionary = {} var _cache_loc: String -var _encrypt_key: String = "" if OS.get_name() in ["HTML5", "UWP", "Web"] else OS.get_unique_id() +var _encrypt_key: String = "" if Utilities.is_web() else OS.get_unique_id() var _base_url : String = "" diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 3cad552..7f13ae2 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -83,7 +83,7 @@ func child(path : String) -> StorageReference: func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: if not valid: return null - if not "Content-Length" in metadata and OS.get_name() != "HTML5": + if not "Content-Length" in metadata and not Utilities.is_web(): metadata["Content-Length"] = data.size() var headers := [] diff --git a/assets/buttons/normal_button.tscn b/assets/buttons/normal_button.tscn index 163c37b..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="Texture2D" id=2] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" id=3] -[ext_resource path="res://assets/buttons/normal_button_disabled.png" type="Texture2D" 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 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 ) -__meta__ = { -"_edit_use_anchors_": false -} +texture_normal = ExtResource("2") +texture_disabled = ExtResource("4") +script = ExtResource("1") [node name="label" type="RichTextLabel" parent="."] +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/main.tscn b/main.tscn index 7189d11..a9fc54b 100644 --- a/main.tscn +++ b/main.tscn @@ -3,7 +3,7 @@ [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" path="res://assets/buttons/normal_button.tscn" id="4"] +[ext_resource type="PackedScene" uid="uid://fnf1abx32uum" path="res://assets/buttons/normal_button.tscn" id="4"] [node name="main" type="Node2D"] script = ExtResource("1") diff --git a/project.godot b/project.godot index 2ef49f0..3b1271f 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="FirebaseTestHarness" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.2") +config/features=PackedStringArray("4.3") config/icon="res://icon.png" [autoload] @@ -29,4 +29,6 @@ common/enable_pause_aware_picking=true [rendering] +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" environment/default_environment="res://default_env.tres" diff --git a/tests/auth/auth.tscn b/tests/auth/auth.tscn index e7ac12f..f21c5e2 100644 --- a/tests/auth/auth.tscn +++ b/tests/auth/auth.tscn @@ -1,42 +1,34 @@ -[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="Texture2D" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" id=4] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="FontFile" id=5] +[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="Node2D"] -script = ExtResource( 1 ) +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] offset_right = 1024.0 offset_bottom = 600.0 -texture = ExtResource( 3 ) -__meta__ = { -"_edit_use_anchors_": false -} +texture = ExtResource("3") [node name="title" type="Label" parent="."] offset_left = 376.0 offset_top = 13.0 offset_right = 498.0 offset_bottom = 52.0 -custom_fonts/font = ExtResource( 4 ) text = "Auth Tests" -align = 1 +horizontal_alignment = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 2 )] +[node name="back" parent="." instance=ExtResource("2")] +anchors_preset = 15 offset_right = 190.354 offset_bottom = 44.6465 label = "Back" -[node name="test_auth" parent="." instance=ExtResource( 2 )] +[node name="test_auth" parent="." instance=ExtResource("2")] +anchors_preset = 15 offset_left = 0.0 offset_top = 65.0 offset_right = 190.0 @@ -50,13 +42,8 @@ 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", -]] +[node name="signup_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 131.0 offset_right = 34.0 @@ -64,21 +51,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) text = "Signup" -[node name="login_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 155.0 offset_right = 34.0 @@ -86,21 +68,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) text = "Login" -[node name="auth_file_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="auth_file_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 179.0 offset_right = 34.0 @@ -108,21 +85,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) text = "Auth File Check" -[node name="user_data_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="user_data_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 203.0 offset_right = 34.0 @@ -130,21 +102,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) text = "User Data Check" -[node name="change_pass_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="change_pass_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 227.0 offset_right = 34.0 @@ -152,21 +119,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) text = "Change Password" -[node name="change_email_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="change_email_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 251.0 offset_right = 34.0 @@ -174,21 +136,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) text = "Change Email" -[node name="login_check_2" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="login_check_2" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 275.0 offset_right = 34.0 @@ -196,21 +153,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) text = "Login Again" -[node name="delete_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="delete_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 299.0 offset_right = 34.0 @@ -218,16 +170,13 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 5 ) text = "Delete" [connection signal="pressed" from="back" to="." method="_on_back_pressed"] diff --git a/tests/database/database.tscn b/tests/database/database.tscn index 070a5f6..13980d1 100644 --- a/tests/database/database.tscn +++ b/tests/database/database.tscn @@ -3,7 +3,7 @@ [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" path="res://assets/buttons/normal_button.tscn" id="5"] +[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"] From d9414f5fafbd9d6ccd395d469db566c72dc83db7 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sun, 21 Apr 2024 12:01:17 -0400 Subject: [PATCH 07/22] Fixing some issues hopefully --- .gitattributes | 2 + addons/godot-firebase/Utilies.gd | 34 +- addons/godot-firebase/auth/auth.gd | 808 +++++++++--------- .../godot-firebase/auth/providers/facebook.gd | 34 +- addons/godot-firebase/database/database.gd | 60 +- addons/godot-firebase/database/reference.gd | 256 +++--- .../dynamiclinks/dynamiclinks.gd | 1 + addons/godot-firebase/example.env | 24 + addons/godot-firebase/firebase/firebase.gd | 162 ++-- addons/godot-firebase/firebase/firebase.tscn | 1 - addons/godot-firebase/firestore/firestore.gd | 275 +++--- .../firestore/firestore_collection.gd | 130 +-- .../firestore/firestore_document.gd | 230 ++--- .../firestore/firestore_query.gd | 282 +++--- .../firestore/firestore_task.gd | 237 ++--- addons/godot-firebase/functions/functions.gd | 197 ++--- addons/godot-firebase/plugin.cfg | 2 +- addons/godot-firebase/storage/storage.gd | 534 ++++++------ .../storage/storage_reference.gd | 154 ++-- addons/gut/icon.png.import | 35 + addons/http-sse-client/HTTPSSEClient.gd | 182 ++-- addons/http-sse-client/icon.png.import | 2 +- assets/buttons/normal_button.gd | 2 +- main.gd | 12 +- tests/auth/auth.gd | 248 +++--- tests/auth/auth.tscn | 2 - tests/database/database.gd | 180 ++-- tests/database/database.tscn | 1 - tests/firestore/firestore.gd | 228 ++--- 29 files changed, 2209 insertions(+), 2106 deletions(-) create mode 100644 .gitattributes create mode 100644 addons/godot-firebase/example.env create mode 100644 addons/gut/icon.png.import 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/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd index ff4e09a..2b69d36 100644 --- a/addons/godot-firebase/Utilies.gd +++ b/addons/godot-firebase/Utilies.gd @@ -2,20 +2,22 @@ extends Node class_name Utilities static func get_json_data(value): - Firebase._print(value) - if value is PackedByteArray: - value = value.get_string_from_utf8() - var json = JSON.new() - var json_parse_result = json.parse(value) - Firebase._print(json_parse_result) - if json_parse_result == OK: - Firebase._print(json.data) - return json.data - else: - json["data"]["error"] = json_parse_result - Firebase._print(json) - - return json - + 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 + + +# 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"] + return OS.get_name() in ["HTML5", "Web"] diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 7f72461..4b3a21c 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -55,88 +55,88 @@ var tcp_timer : Timer = Timer.new() var tcp_timeout : float = 0.5 var _headers : PackedStringArray = [ - "Content-Type: application/json", - "Accept: application/json", + "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,80 +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.timeout.connect(_tcp_stream_timer) - - if Utilities.is_web(): - _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 - request_completed.connect(_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 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 - 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) + 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() @@ -225,435 +226,446 @@ 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 - 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) + 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 - 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) + 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 - 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) + 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) - 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, "*") + 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 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) + 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: - 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) + 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 - 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) - + 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 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: 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() - + 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() - logged_out.emit() + 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 - 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) + 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 : 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) - 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 + 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 = 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)) +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 = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _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) - +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: - if (FileAccess.file_exists("user://user.auth")): - DirAccess.remove_absolute("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: - if (FileAccess.file_exists("user://user.auth")): - # Will ensure "auth_request" emitted - 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") +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 - 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) + 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 - 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) + 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 : 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) + 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 - 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) + 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 - 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) + 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 - 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) + 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 - 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) + 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) # 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 - 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) + 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 = 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 + 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 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/providers/facebook.gd b/addons/godot-firebase/auth/providers/facebook.gd index 926c4a1..35e8dd8 100644 --- a/addons/godot-firebase/auth/providers/facebook.gd +++ b/addons/godot-firebase/auth/providers/facebook.gd @@ -2,20 +2,20 @@ class_name FacebookProvider extends AuthProvider 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" - - + 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/database/database.gd b/addons/godot-firebase/database/database.gd index a2bf082..755d4e4 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -13,46 +13,46 @@ 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 getter := HTTPRequest.new() - getter.use_threads = true - var pusher : HTTPRequest = HTTPRequest.new() - pusher.use_threads = true - 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_getter(getter) - firebase_reference.set_listener(listener) - firebase_reference.set_store(store) - add_child(firebase_reference) - return firebase_reference + var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() + var getter := HTTPRequest.new() + getter.use_threads = true + var pusher : HTTPRequest = HTTPRequest.new() + pusher.use_threads = true + 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_getter(getter) + firebase_reference.set_listener(listener) + firebase_reference.set_store(store) + add_child(firebase_reference) + return firebase_reference diff --git a/addons/godot-firebase/database/reference.gd b/addons/godot-firebase/database/reference.gd index 803da37..1dacd0b 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -57,185 +57,185 @@ const _key_filter_tag : String = "$key" var _headers : PackedStringArray = [] 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 + _auth = auth_ref + _config = config_ref func set_pusher(pusher_ref : HTTPRequest) -> void: - if !_pusher: - _pusher = pusher_ref - add_child(_pusher) - _pusher.request_completed.connect(on_push_request_complete) + if !_pusher: + _pusher = pusher_ref + add_child(_pusher) + _pusher.request_completed.connect(on_push_request_complete) func set_getter(getter_ref : HTTPRequest) -> void: - if !_getter: - _getter = getter_ref - add_child(_getter) - _getter.request_completed.connect(on_get_request_complete) - + if !_getter: + _getter = getter_ref + add_child(_getter) + _getter.request_completed.connect(on_get_request_complete) + func set_listener(listener_ref : Node) -> void: - if !_listener: - _listener = listener_ref - add_child(_listener) - _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) + if !_listener: + _listener = listener_ref + add_child(_listener) + _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) 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(): - 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)) - pass + 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)) + pass func set_store(store_ref : FirebaseDatabaseStore) -> void: - if !_store: - _store = store_ref - add_child(_store) + if !_store: + _store = store_ref + add_child(_store) func update(path : String, data : Dictionary) -> void: - path = path.strip_edges(true, true) + path = path.strip_edges(true, true) - if path == _separator: - path = "" + if path == _separator: + path = "" - var to_update = JSON.stringify(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()) + var to_update = JSON.stringify(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, HTTPClient.METHOD_PATCH, to_update) - else: - _update_queue.append({"path": path, "data": data}) + _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) + else: + _update_queue.append({"path": path, "data": data}) func push(data : Dictionary) -> void: - var to_push = JSON.stringify(data) - if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) - else: - _push_queue.append(data) + var to_push = JSON.stringify(data) + if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, 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, HTTPClient.METHOD_DELETE, "") - else: - _delete_queue.append(reference) + if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: + _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") + else: + _delete_queue.append(reference) # # Gets a data snapshot once at the position passed in # func once(reference : String) -> void: - if _getter.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() - _getter.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") - else: - _get_queue.append(reference) + if _getter.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: + var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() + _getter.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") + else: + _get_queue.append(reference) # # 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_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 + 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_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] - - 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] + + return _cached_filter func _filter_query_empty() -> bool: - return _filter_query == null or _filter_query.is_empty() + 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) + 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() - - # handle queued operations - 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 response_code == HTTPClient.RESPONSE_OK: + push_successful.emit() + else: + push_failed.emit() + + # handle queued operations + 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()) func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - if response_code == HTTPClient.RESPONSE_OK: - var bod = Utilities.get_json_data(body) - once_successful.emit(bod) - else: - once_failed.emit() - - # handle queued operations - if _get_queue.size() > 0: - once(_get_queue.pop_front()) - + if response_code == HTTPClient.RESPONSE_OK: + var bod = Utilities.get_json_data(body) + once_successful.emit(bod) + else: + once_failed.emit() + + # handle queued operations + if _get_queue.size() > 0: + once(_get_queue.pop_front()) + diff --git a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index c23e86c..67a1374 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -32,6 +32,7 @@ enum Requests { func _set_config(config_json : Dictionary) -> void: _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() 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 444d550..efbaa38 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -17,27 +17,27 @@ const _AUTH_PROVIDERS : String = "firebase/auth_providers" ## @type FirebaseAuth ## The Firebase Authentication API. -@onready var Auth := $Auth +@onready var Auth : FirebaseAuth = $Auth ## @type FirebaseFirestore ## The Firebase Firestore API. -@onready var Firestore := $Firestore +@onready var Firestore : FirebaseFirestore = $Firestore ## @type FirebaseDatabase ## The Firebase Realtime Database API. -@onready var Database := $Database +@onready var Database : FirebaseDatabase = $Database ## @type FirebaseStorage ## The Firebase Storage API. -@onready var Storage := $Storage +@onready var Storage : FirebaseStorage = $Storage ## @type FirebaseDynamicLinks ## The Firebase Dynamic Links API. -@onready var DynamicLinks := $DynamicLinks +@onready var DynamicLinks : FirebaseDynamicLinks = $DynamicLinks ## @type FirebaseFunctions ## The Firebase Cloud Functions API -@onready var Functions := $Functions +@onready var Functions : FirebaseFunctions = $Functions @export var emulating : bool = false @@ -45,96 +45,96 @@ const _AUTH_PROVIDERS : String = "firebase/auth_providers" # These values can be found in your Firebase Project # 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":"", - "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 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" and config_value.has("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() + 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.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) + 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) -> void: - print("[Firebase] >> " + str(msg)) +func _print(msg : String) -> void: + print("[Firebase] >> " + str(msg)) diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index cd922a6..d5b34d7 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -12,7 +12,6 @@ script = ExtResource("3") [node name="Auth" type="HTTPRequest" parent="."] -accept_gzip = false max_redirects = 12 timeout = 10.0 script = ExtResource("4") diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 5586777..5dd2e9b 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -30,9 +30,9 @@ signal result_query(result) signal task_error(code,status,message) enum Requests { - 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. + 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 @@ -52,7 +52,7 @@ 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 @@ -86,18 +86,18 @@ var _http_request_pool := [] var _offline: bool = false : set = _set_offline func _ready() -> void: - pass + 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_at(i) - continue # Just to skip set_meta on a queue_freed request - 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) + continue # Just to skip set_meta on a queue_freed request + request.set_meta("lifetime", lifetime) ## Returns a reference collection by its [i]path[/i]. @@ -107,18 +107,18 @@ func _process(delta : float) -> void: ## @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] + 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 checked your Firestore database. @@ -141,18 +141,18 @@ func collection(path : String) -> FirestoreCollection: ## @arg-types FirestoreQuery ## @return FirestoreTask func query(query : FirestoreQuery) -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.result_query.connect(_on_result_query) # In theory, this and the following could be a CONNECT_ONE_SHOT, but I'm iffy on whether or not that might break any client code, so leaving this for now. - firestore_task.task_error.connect(_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 + var firestore_task : FirestoreTask = FirestoreTask.new() + firestore_task.result_query.connect(_on_result_query) # In theory, this and the following could be a CONNECT_ONE_SHOT, but I'm iffy on whether or not that might break any client code, so leaving this for now. + firestore_task.task_error.connect(_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.stringify(body) - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task + firestore_task.data = query + firestore_task._fields = JSON.stringify(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 checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. @@ -170,151 +170,152 @@ func query(query : FirestoreQuery) -> FirestoreTask: ## @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.listed_documents.connect(_on_listed_documents) # Same as above with one shot connections - firestore_task.task_error.connect(_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 + var firestore_task : FirestoreTask = FirestoreTask.new() + firestore_task.listed_documents.connect(_on_listed_documents) # Same as above with one shot connections + firestore_task.task_error.connect(_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() + 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 + 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 + 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: - return # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. + return # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. func _set_config(config_json : Dictionary) -> void: - _config = config_json - _cache_loc = _config["cacheLocation"] - _extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId) + _config = config_json + _cache_loc = _config["cacheLocation"] + _extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId) - # 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. + # 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() + _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, PackedStringArray(), PackedByteArray()) - return - - 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 - 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.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, task._method, task._fields) + if _offline: + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) + return + + 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 + 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 + Utilities.fix_http_request(http_request) + _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, task._method, task._fields) # ------------- func _on_listed_documents(_listed_documents : Array): - listed_documents.emit(_listed_documents) + listed_documents.emit(_listed_documents) func _on_result_query(result : Array): - result_query.emit(result) + result_query.emit(result) func _on_task_error(code : int, status : String, message : String, task : int): - task_error.emit(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 - for key in collections: - collections[key].auth = auth + auth = auth_result + for key in collections: + collections[key].auth = auth func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - auth = auth_result - for key in collections: - collections[key].auth = auth + auth = auth_result + for key in collections: + collections[key].auth = auth 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) + 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) + #_connect_check_node.request(_base_url) 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) diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 8808112..e079f92 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -36,107 +36,107 @@ var _request_queues := {} ## @return FirestoreTask ## used to GET a document from the collection, specify @document_id func get_doc(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") + 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.get_document.connect(_on_get_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task + task.get_document.connect(_on_get_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url) + return task ## @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 + 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.add_document.connect(_on_add_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) - return task + task.add_document.connect(_on_add_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.stringify(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.update_document.connect(_on_update_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) - return task + 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.update_document.connect(_on_update_document) + task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) + return task ## @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") + 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.delete_document.connect(_on_delete_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task + task.delete_document.connect(_on_delete_document) + task.task_finished.connect(_on_task_finished.bind(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 + return _base_url + _extended_url + collection_name func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: - if not task.task_error.is_connected(_on_error): - task.task_error.connect(_on_error) - - 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]) - if _request_queues.has(document_id) and not _request_queues[document_id].is_empty(): - _request_queues[document_id].append(task) - else: - _request_queues[document_id] = [] - firestore._pooled_request(task) + if not task.task_error.is_connected(_on_error): + task.task_error.connect(_on_error) + + 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]) + if _request_queues.has(document_id) and not _request_queues[document_id].is_empty(): + _request_queues[document_id].append(task) + else: + _request_queues[document_id] = [] + firestore._pooled_request(task) func _on_task_finished(task : FirestoreTask, document_id : String) -> void: - if not _request_queues[document_id].is_empty(): - task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) + if not _request_queues[document_id].is_empty(): + task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) # -------------------- Higher level of communication with signals func _on_get_document(document : FirestoreDocument): - get_document.emit(document) + get_document.emit(document) func _on_add_document(document : FirestoreDocument): - add_document.emit(document) + add_document.emit(document) func _on_update_document(document : FirestoreDocument): - update_document.emit(document) + update_document.emit(document) func _on_delete_document(): - delete_document.emit() + delete_document.emit() func _on_error(code, status, message, task): - error.emit(code, status, message) - Firebase._printerr(message) + error.emit(code, status, message) + Firebase._printerr(message) diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index 12039b6..8ec075b 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -17,145 +17,145 @@ var doc_name : String # only .name var create_time : String # createTime func _init(doc : Dictionary = {},_doc_name : String = "",_doc_fields : Dictionary = {}): - 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 + 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 # 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 : 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.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)} + 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.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} + 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} # 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 + 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 # 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 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" - fields.append({ var_type : field }) - return fields + 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 + 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() - return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values + dict.erase('weekday') + dict.erase('dst') + var dict_values : Array = dict.values() + 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: - 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 + 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']) + return field.has_all(['year','month','day','hour','minute','second']) # 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}, \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}) diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index 6a22e81..fc6d034 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -7,60 +7,60 @@ 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 + select = {}, + from = [], + where = {}, + orderBy = [], + startAt = {}, + endAt = {}, + offset = 0, + limit = 0 } var query : 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_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 } enum DIRECTION { - DIRECTION_UNSPECIFIED, - ASCENDING, - DESCENDING + DIRECTION_UNSPECIFIED, + ASCENDING, + DESCENDING } @@ -68,35 +68,35 @@ enum DIRECTION { # 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 @@ -106,43 +106,43 @@ func from_many(collections_array : Array) -> FirestoreQuery: # @chain : from FirestoreQuery.OPERATOR.[OR/AND], use it only if you want to chain "AND" or "OR" logic with futher where() calls # 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 @@ -150,88 +150,88 @@ 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 # 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 = FirestoreDocument.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 = FirestoreDocument.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_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 3fe53a4..7acc032 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -24,22 +24,22 @@ 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 checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]add(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result.. ## @arg-types FirestoreDocument signal add_document(doc) -## Emitted when a [code]get(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]get(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. ## @arg-types FirestoreDocument signal get_document(doc) -## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. ## @arg-types FirestoreDocument signal update_document(doc) -## Emitted when a [code]delete(document)[/code] request checked 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 checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]delete(document)[/code] request checked a [class FirebaseCollection] is successfully completed and [code]true[/code] will be passed. [code]error()[/code] signal will be emitted otherwise and [code]false[/code] will be passed as a result. +## @arg-types bool +signal delete_document(success) +## Emitted when a [code]list(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code][][/code] will be passed as a result.. ## @arg-types Array signal listed_documents(documents) -## Emitted when a [code]query(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise. +## Emitted when a [code]query(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code][][/code] will be passed as a result. ## @arg-types Array signal result_query(result) ## Emitted when a request is [b]not[/b] successfully completed. @@ -47,12 +47,22 @@ signal result_query(result) signal task_error(code, status, message, task) 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_LIST ## A POST Request Task, processing a list() request +} + +## 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" } ## The code indicating the request Firestore is processing. @@ -76,109 +86,128 @@ 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 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 - - # Probably going to regret this... - if response_code == HTTPClient.RESPONSE_OK: - data = bod - match action: - Task.TASK_POST: - document = FirestoreDocument.new(bod) - add_document.emit(document) - Task.TASK_GET: - document = FirestoreDocument.new(bod) - get_document.emit(document) - Task.TASK_PATCH: - document = FirestoreDocument.new(bod) - update_document.emit(document) - Task.TASK_DELETE: - delete_document.emit() - Task.TASK_QUERY: - data = [] - for doc in bod: - if doc.has('document'): - data.append(FirestoreDocument.new(doc.document)) - result_query.emit(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) - listed_documents.emit(data) - else: - Firebase._printerr("Action in error was: " + str(action)) - emit_error(task_error, bod, action) - - task_finished.emit(self) + var bod = body.get_string_from_utf8() + if bod != "": + bod = Utilities.get_json_data(bod) + + 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 + + # Probably going to regret this... + if response_code == HTTPClient.RESPONSE_OK: + data = bod + match action: + Task.TASK_POST: + document = FirestoreDocument.new(bod) + add_document.emit(document) + Task.TASK_GET: + document = FirestoreDocument.new(bod) + get_document.emit(document) + Task.TASK_PATCH: + document = FirestoreDocument.new(bod) + update_document.emit(document) + Task.TASK_DELETE: + delete_document.emit(true) + Task.TASK_QUERY: + data = [] + for doc in bod: + if doc.has('document'): + data.append(FirestoreDocument.new(doc.document)) + result_query.emit(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) + listed_documents.emit(data) + else: + var description = "" + if TASK_MAP.has(action): + description = "(" + TASK_MAP[action] + ")" + + Firebase._printerr("Action in error was: " + str(action) + " " + description) + emit_error(task_error, bod, action) + match action: + Task.TASK_POST: + add_document.emit(null) + Task.TASK_GET: + get_document.emit(null) + Task.TASK_PATCH: + update_document.emit(null) + Task.TASK_DELETE: + delete_document.emit(false) + Task.TASK_QUERY: + data = [] + result_query.emit(data) + Task.TASK_LIST: + data = [] + listed_documents.emit(data) + + task_finished.emit(self) func emit_error(_signal, 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 + 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 - _signal.emit(error.code, error.status, error.message, task) + _signal.emit(error.code, error.status, error.message, task) - return + return - _signal.emit(1, 0, "Unknown error", task) + _signal.emit(1, 0, "Unknown error", task) 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 + 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: - return body # Removing caching for now, hopefully this works without killing everyone and everything + return body # Removing caching for now, hopefully this works without killing everyone and everything 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_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 + 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/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index 365ae83..d1d9586 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -30,7 +30,7 @@ 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 @@ -51,167 +51,168 @@ var _http_request_pool : Array = [] var _offline: bool = false : set = _set_offline func _ready() -> void: - pass + 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_at(i) - return # Prevent setting a value on request after it's already been queue_freed - 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.task_error.connect(_on_task_error) - function_task.task_finished.connect(_on_task_finished) - function_task.function_executed.connect(_on_function_executed) + 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.is_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.is_empty(): - function_task._headers = PackedStringArray(["Content-Type: application/json"]) - function_task._fields = JSON.stringify(body) + if not body.is_empty(): + function_task._headers = PackedStringArray(["Content-Type: application/json"]) + 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, PackedStringArray(), PackedByteArray()) - return + if _offline: + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) + return - 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") + 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 = Array(task._headers) + [_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.request_completed.connect(_on_pooled_request_completed.bind(http_request)) + if not http_request: + http_request = HTTPRequest.new() + Utilities.fix_http_request(http_request) + _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, 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): - task_error.emit(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 : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: - request.get_meta("task")._on_request_completed(result, response_code, headers, body) - request.set_meta("requesting", false) + 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) + _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/plugin.cfg b/addons/godot-firebase/plugin.cfg index 5636e73..b8ffaac 100644 --- a/addons/godot-firebase/plugin.cfg +++ b/addons/godot-firebase/plugin.cfg @@ -3,5 +3,5 @@ name="GodotFirebase" description="Google Firebase SDK written in GDScript for use in Godot Engine 4.0 projects." author="GodotNutsOrg" -version="1.0" +version="1.1" script="plugin.gd" diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 1b4b93e..82a8017 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -47,311 +47,311 @@ 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, 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() - - _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) + 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() + + _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 checked the server end. func ref(path := "") -> StorageReference: - 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.valid = true - ref.bucket = bucket - ref.full_path = path - ref.name = path.get_file() - ref.parent = ref(path.path_join("..")) - 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.valid = true + ref.bucket = bucket + ref.full_path = path + ref.name = path.get_file() + ref.parent = ref(path.path_join("..")) + ref.root = _root_ref + ref.storage = self + 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 }) + ## 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) -> StorageTask: - if _is_invalid_authentication(): - 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 + if _is_invalid_authentication(): + 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 _is_invalid_authentication(): - 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) - - 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() - task_failed.emit(task.result, task.response_code, task.data) - _pending_tasks.erase(task) - - return task + if _is_invalid_authentication(): + 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) + + 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() + task_failed.emit(task.result, task.response_code, task.data) + _pending_tasks.erase(task) + + return task func _list(ref : StorageReference, list_all : bool) -> StorageTask: - if _is_invalid_authentication(): - return null + if _is_invalid_authentication(): + 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 + 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 _is_invalid_authentication(): - return null + if _is_invalid_authentication(): + 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 + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + task.action = StorageTask.Task.TASK_DELETE + _process_request(task) + return task 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 = 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) + 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_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 : StorageTask - if not _pending_tasks.is_empty(): - next_task = _pending_tasks.pop_front() - - 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) - else: - task_successful.emit(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.is_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) + task.ref.valid = false + 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 : StorageTask + if not _pending_tasks.is_empty(): + next_task = _pending_tasks.pop_front() + + 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) + else: + task_successful.emit(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.is_empty(): + next_task = _pending_tasks.pop_front() + else: + break 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 := "/".join(PackedStringArray(new_dirs)) - 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()) + return (_config == null or _config.is_empty()) or (_auth == null or _auth.is_empty()) diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 7f13ae2..2ce2446 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -13,31 +13,31 @@ const DEFAULT_MIME_TYPE = "application/octet-stream" ## 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": "video/webm", + "xml": "text/xml", } ## @default "" @@ -73,111 +73,111 @@ var valid : bool = false ## @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.path_join(path)) + if not valid: + return null + 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 checked this task is found in the returned [StorageTask]. func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: - if not valid: - return null - if not "Content-Length" in metadata and not Utilities.is_web(): - metadata["Content-Length"] = data.size() + if not valid: + return null + 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 storage._upload(data, headers, self, false) ## @args data, metadata ## @return StorageTask ## Like [method put_data], but [code]data[/code] is a [String]. func put_string(data : String, metadata := {}) -> StorageTask: - return put_data(data.to_utf8_buffer(), metadata) + return put_data(data.to_utf8_buffer(), metadata) ## @args file_path, metadata ## @return StorageTask ## 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 := FileAccess.open(file_path, FileAccess.READ) - var data := file.get_buffer(file.get_length()) + 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 put_data(data, metadata) ## @return StorageTask ## 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() -> StorageTask: - if not valid: - return null - storage._download(self, false, false) - return storage._pending_tasks[-1] + if not valid: + return null + storage._download(self, false, false) + return storage._pending_tasks[-1] ## @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.task_finished.connect(_on_task_finished.bind(task, "stringify")) - return task + var task := get_data() + task.task_finished.connect(_on_task_finished.bind(task, "stringify")) + return task ## @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 checked this task is found in the returned [StorageTask]. func get_download_url() -> StorageTask: - if not valid: - return null - return storage._download(self, false, true) + if not valid: + return null + return storage._download(self, false, true) ## @return StorageTask ## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask]. func get_metadata() -> StorageTask: - if not valid: - return null - return storage._download(self, true, false) + if not valid: + return null + return 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 checked the server end. Status checked this task is found in the returned [StorageTask]. func update_metadata(metadata : Dictionary) -> StorageTask: - if not valid: - return null - var data := JSON.stringify(metadata).to_utf8_buffer() - var headers := PackedStringArray(["Accept: application/json"]) - return storage._upload(data, headers, self, true) + if not valid: + return null + var data := JSON.stringify(metadata).to_utf8_buffer() + var headers := PackedStringArray(["Accept: application/json"]) + return 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 checked this task is found in the returned [StorageTask]. func list() -> StorageTask: - if not valid: - return null - return storage._list(self, false) + if not valid: + return null + return 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 checked this task is found in the returned [StorageTask]. func list_all() -> StorageTask: - if not valid: - return null - return storage._list(self, true) + if not valid: + return null + return 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 checked this task is found in the returned [StorageTask]. func delete() -> StorageTask: - if not valid: - return null - return storage._delete(self) + if not valid: + return null + return storage._delete(self) func _to_string() -> String: - var string := "gs://%s/%s" % [bucket, full_path] - if not valid: - string += " [Invalid RefCounted]" - return string + var string := "gs://%s/%s" % [bucket, full_path] + if not valid: + string += " [Invalid RefCounted]" + return string func _on_task_finished(task : StorageTask, action : String) -> void: - match action: - "stringify": - if typeof(task.data) == TYPE_PACKED_BYTE_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/gut/icon.png.import b/addons/gut/icon.png.import new file mode 100644 index 0000000..3f2dabe --- /dev/null +++ b/addons/gut/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gut/icon.png" +dest_files=[ "res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +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 +svg/scale=1.0 diff --git a/addons/http-sse-client/HTTPSSEClient.gd b/addons/http-sse-client/HTTPSSEClient.gd index 2a01706..9906de9 100644 --- a/addons/http-sse-client/HTTPSSEClient.gd +++ b/addons/http-sse-client/HTTPSSEClient.gd @@ -23,107 +23,107 @@ var is_requested = false var response_body = PackedByteArray() 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 + 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 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)) + 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_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 + 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) + 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) + 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): - 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) - var event_value = event.replace(event_tag, "").strip_edges() - assert(event_value) - assert(event_value.length() > 0) - result["event"] = event_value - var data = body.right(body.length() - (data_idx + data_tag.length())).strip_edges() - assert(data) - assert(data.length() > 0) - result["data"] = data - return result + 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) + var event_value = event.replace(event_tag, "").strip_edges() + assert(event_value) + assert(event_value.length() > 0) + result["event"] = event_value + var data = body.right(body.length() - (data_idx + data_tag.length())).strip_edges() + assert(data) + assert(data.length() > 0) + result["data"] = data + return result func _exit_tree(): - if httpclient: - httpclient.close() + if httpclient: + httpclient.close() diff --git a/addons/http-sse-client/icon.png.import b/addons/http-sse-client/icon.png.import index 24f6468..2987cee 100644 --- a/addons/http-sse-client/icon.png.import +++ b/addons/http-sse-client/icon.png.import @@ -2,7 +2,7 @@ importer="texture" type="CompressedTexture2D" -uid="uid://b21bcvx380sb4" +uid="uid://dyexk436ak68q" path="res://.godot/imported/icon.png-fd61c0c5f1d1b4df37e0abe3d8f80985.ctex" metadata={ "vram_texture": false diff --git a/assets/buttons/normal_button.gd b/assets/buttons/normal_button.gd index 9bc4547..28c62f3 100644 --- a/assets/buttons/normal_button.gd +++ b/assets/buttons/normal_button.gd @@ -2,4 +2,4 @@ extends TextureButton @export var label : String = '' func _ready() -> void: - $label.text = "[center]" + label + "[/center]" + $label.text = "[center]" + label + "[/center]" diff --git a/main.gd b/main.gd index adcd5c3..9c0f259 100644 --- a/main.gd +++ b/main.gd @@ -1,23 +1,23 @@ extends Node2D func _ready(): - pass + pass func _on_auth_tests_pressed(): - get_tree().change_scene_to_file("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_to_file("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_to_file("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_to_file("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_to_file("res://tests/links/links.tscn") + get_tree().change_scene_to_file("res://tests/links/links.tscn") diff --git a/tests/auth/auth.gd b/tests/auth/auth.gd index ce62548..1a31288 100644 --- a/tests/auth/auth.gd +++ b/tests/auth/auth.gd @@ -18,161 +18,161 @@ const _timer_length = 5 # Function called when the scene is ready func _ready(): - 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) + 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 # 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_auth.disabled = true + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_auth.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_auth.disabled = false + _test_running = false + $back.disabled = false + $test_auth.disabled = false # Function called when the end user presses the 'Test Auth' button # Main function that is run when testing Authentication func _on_test_auth_pressed() -> void: - # Print to the console GUI that the test is starting - _print_to_console("STARTING AUTH TESTS") - _test_started() - - # 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) - await get_tree().create_timer(_timer_length).timeout - - # Start Login test using the first email - if _auth_error != true: - $signup_check.button_pressed = true - _print_to_console("\nTrying to login...") - Firebase.Auth.login_with_email_and_password(_email1, _password1) - await get_tree().create_timer(_timer_length).timeout - - # Check Auth File - if _auth_error != true: - $login_check.button_pressed = true - _check_auth_file() - await get_tree().create_timer(_timer_length).timeout - - # Get User Data Test - if _auth_error != true: - $auth_file_check.button_pressed = true - _print_to_console("\nTrying to get user data...") - Firebase.Auth.get_user_data() - await get_tree().create_timer(_timer_length).timeout - - # Change User Password - if _auth_error != true: - $user_data_check.button_pressed = true - _print_to_console("\nTrying to change user password...") - Firebase.Auth.change_user_password(_password2) - await get_tree().create_timer(_timer_length).timeout - Firebase.Auth.logout() - await get_tree().create_timer(_timer_length).timeout - Firebase.Auth.login_with_email_and_password(_email1, _password2) - 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.button_pressed = true - _print_to_console("\nTrying to change user email...") - Firebase.Auth.change_user_email(_email2) - await get_tree().create_timer(_timer_length).timeout - Firebase.Auth.logout() - await get_tree().create_timer(_timer_length).timeout - Firebase.Auth.login_with_email_and_password(_email2, _password2) - await get_tree().create_timer(_timer_length).timeout - - # Login with the new credentials - if _auth_error != 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) - await get_tree().create_timer(_timer_length).timeout - - # Start Delete Account test - if _auth_error != 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.button_pressed = true - _print_to_console("\nFINISHED AUTH TESTS") - _test_finished() - else: - _auth_test_error() + # Print to the console GUI that the test is starting + _print_to_console("STARTING AUTH TESTS") + _test_started() + + # 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) + await get_tree().create_timer(_timer_length).timeout + + # Start Login test using the first email + if _auth_error != true: + $signup_check.button_pressed = true + _print_to_console("\nTrying to login...") + Firebase.Auth.login_with_email_and_password(_email1, _password1) + await get_tree().create_timer(_timer_length).timeout + + # Check Auth File + if _auth_error != true: + $login_check.button_pressed = true + _check_auth_file() + await get_tree().create_timer(_timer_length).timeout + + # Get User Data Test + if _auth_error != true: + $auth_file_check.button_pressed = true + _print_to_console("\nTrying to get user data...") + Firebase.Auth.get_user_data() + await get_tree().create_timer(_timer_length).timeout + + # Change User Password + if _auth_error != true: + $user_data_check.button_pressed = true + _print_to_console("\nTrying to change user password...") + Firebase.Auth.change_user_password(_password2) + await get_tree().create_timer(_timer_length).timeout + Firebase.Auth.logout() + await get_tree().create_timer(_timer_length).timeout + Firebase.Auth.login_with_email_and_password(_email1, _password2) + 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.button_pressed = true + _print_to_console("\nTrying to change user email...") + Firebase.Auth.change_user_email(_email2) + await get_tree().create_timer(_timer_length).timeout + Firebase.Auth.logout() + await get_tree().create_timer(_timer_length).timeout + Firebase.Auth.login_with_email_and_password(_email2, _password2) + await get_tree().create_timer(_timer_length).timeout + + # Login with the new credentials + if _auth_error != 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) + await get_tree().create_timer(_timer_length).timeout + + # Start Delete Account test + if _auth_error != 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.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 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)...") - 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") + _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)...") + 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 has worked") - Firebase.Auth.save_auth(_auth) + _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)...") - 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") - _auth_error = true + _print_to_console("Checking for auth file (It should be there)...") + 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") + _auth_error = true func _on_userdata_received(userdata) -> void: - if userdata != null: - _print_to_console("User data received") - else: - _print_to_console_error("Could not get user data") - _auth_error = true + if userdata != null: + _print_to_console("User data received") + else: + _print_to_console_error("Could not get user data") + _auth_error = true func _on_login_failed(error_code, message): - _print_to_console_error("error code: " + str(error_code)) - _print_to_console_error("message: " + str(message)) - _auth_error = true + _print_to_console_error("error code: " + str(error_code)) + _print_to_console_error("message: " + str(message)) + _auth_error = true func _auth_test_error() -> void: - _print_to_console_error("There has been a failure") - test_finished.emit() + _print_to_console_error("There has been a failure") + 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.text - var updated_data = previous_data + data + "\n" - console.text = updated_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 + 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") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/auth/auth.tscn b/tests/auth/auth.tscn index f21c5e2..9b691fa 100644 --- a/tests/auth/auth.tscn +++ b/tests/auth/auth.tscn @@ -22,13 +22,11 @@ horizontal_alignment = 1 uppercase = true [node name="back" parent="." instance=ExtResource("2")] -anchors_preset = 15 offset_right = 190.354 offset_bottom = 44.6465 label = "Back" [node name="test_auth" parent="." instance=ExtResource("2")] -anchors_preset = 15 offset_left = 0.0 offset_top = 65.0 offset_right = 190.0 diff --git a/tests/database/database.gd b/tests/database/database.gd index e67cc49..ca4f3af 100644 --- a/tests/database/database.gd +++ b/tests/database/database.gd @@ -19,141 +19,141 @@ const _password : String = 'Password1234' # Function called when the scene is ready func _ready(): - Firebase.Auth.connect("login_succeeded",Callable(self,"_on_FirebaseAuth_login_succeeded")) - Firebase.Auth.connect("login_failed",Callable(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 # 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_database.disabled = true + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_database.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_database.disabled = false + _test_running = false + $back.disabled = false + $test_database.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_database() + _print_to_console("Login with email and password has worked") + $login_check.button_pressed = true + _test_database() # 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)) + _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_database_pressed(): - _test_started() - Firebase.Auth.login_with_email_and_password(_email, _password) + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) func _test_database(): - # Print to the console GUI that the test is starting - _print_to_console("STARTING DATABASE TESTS") - - # Get the database reference that we will be working with - _print_to_console("\nGetting the Databse RefCounted...") - database_reference = Firebase.Database.get_database_reference("FirebaseTester/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", _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) - database_reference.connect("once_failed", _on_once_failed) - database_reference.connect("once_successful", _on_once_successful) - - # Push data to the RTDB - _print_to_console("\nTrying to push data to the RTD...") - database_reference.push({'user_name':'username', 'message':'Hello world!'}) - $push_data_check.button_pressed = true - await data_key_ready - - # Get data once from the RTDB - _print_to_console("\n\nAttempting a once-off get from the RTD") - database_reference.once(added_data_key + "/user_name") - $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.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") - _test_finished() + # Print to the console GUI that the test is starting + _print_to_console("STARTING DATABASE TESTS") + + # Get the database reference that we will be working with + _print_to_console("\nGetting the Databse RefCounted...") + database_reference = Firebase.Database.get_database_reference("FirebaseTester/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", _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) + database_reference.connect("once_failed", _on_once_failed) + database_reference.connect("once_successful", _on_once_successful) + + # Push data to the RTDB + _print_to_console("\nTrying to push data to the RTD...") + database_reference.push({'user_name':'username', 'message':'Hello world!'}) + $push_data_check.button_pressed = true + await data_key_ready + + # Get data once from the RTDB + _print_to_console("\n\nAttempting a once-off get from the RTD") + database_reference.once(added_data_key + "/user_name") + $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.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") + _test_finished() # 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 + data_key_ready.emit() + _print_to_console(new_data) + _print_to_console(new_data.key) + _print_to_console(new_data.data) # 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 + data_key_ready.emit() + _print_to_console(patch_data) + _print_to_console(patch_data.key) + _print_to_console(patch_data.data) # Function called when pushing data to the RTDB has failed func _on_push_failed(): - _print_to_console_error("Push failed") - database_call_completed.emit() + _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") - + 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() + _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) + 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.text - var updated_data = previous_data + data + "\n" - console.text = updated_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 + 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") + get_tree().change_scene_to_file("res://main.tscn") diff --git a/tests/database/database.tscn b/tests/database/database.tscn index 13980d1..12a8194 100644 --- a/tests/database/database.tscn +++ b/tests/database/database.tscn @@ -24,7 +24,6 @@ text = "Database Tests" uppercase = true [node name="back" parent="." instance=ExtResource("5")] -anchors_preset = 15 offset_right = 190.354 offset_bottom = 44.6465 label = "Back" diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index e6d7677..647ca4c 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -14,170 +14,170 @@ const _password : String = 'Password1234' # Function called when the scene is ready func _ready(): - Firebase.Auth.connect("login_succeeded",Callable(self,"_on_FirebaseAuth_login_succeeded")) - Firebase.Auth.connect("login_failed",Callable(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 # 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_firestore.disabled = true + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_firestore.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_firestore.disabled = false + _test_running = false + $back.disabled = false + $test_firestore.disabled = false # Function called if there is an error in the test # Prints the error to the GUI console so the end user can see func _test_error(data) -> void: - _print_to_console_error(data) - _test_finished() + _print_to_console_error(data) + _test_finished() func _cleanup_previous_run(): - var del_task : FirestoreTask = _collection.delete("Document1") - _document = await del_task.delete_document + var del_task : FirestoreTask = _collection.delete("Document1") + _document = await del_task.delete_document # 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_firestore() + _print_to_console("Login with email and password has worked") + $login_check.button_pressed = true + _test_firestore() # 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)) - _test_error("Login Failed") + _print_to_console_error("error code: " + str(error_code)) + _print_to_console_error("message: " + str(message)) + _test_error("Login Failed") # Function called when the end user presses the 'Test Auth' button # Starts the test func _on_test_firestore_pressed(): - _test_started() - Firebase.Auth.login_with_email_and_password(_email, _password) + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) # Main function that is run when testing Firestore func _test_firestore() -> void: - # Print to the console GUI that the test is starting - _print_to_console("\nSTARTING FIRESTORE TESTS") - - # Connect to the test collection - _print_to_console("\nConnecting to collection 'Firebasetester'") - _collection = Firebase.Firestore.collection('Firebasetester') - - # Connect to signals needed for testing - _collection.connect("add_document",Callable(self,"on_document_add")) - _collection.connect("get_document",Callable(self,"on_document_get")) - _collection.connect("update_document",Callable(self,"on_document_update")) - _collection.connect("delete_document",Callable(self,"on_document_delete")) - _collection.connect("error",Callable(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 = await add_task.add_document - $add_document.button_pressed = true - - # Get Document1 (Document that has been added from the previous step) - _print_to_console("Trying to get Document1") - var get_task = _collection.get_doc('Document1') - _document = await _collection.get_document - if(_document == null): - _test_error("Failed to get document") - return - else: - $get_document.button_pressed = true - - # Print Document1 to the console GUI - _print_to_console("Trying to print contents of Document1") - _print_to_console(_document) - $print_document.button_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 = await up_task.update_document - $update_document.button_pressed = true - - # Get Document1 (With updated that has been added from the previous step) - _print_to_console("Trying to get Document1") - _collection.get_doc('Document1') - _document = await _collection.get_document - $get_document_2.button_pressed = true - - # Print Document1 to the console GUI - _print_to_console("Trying to print contents of Document1") - _print_to_console(_document) - $print_document_2.button_pressed = true - - # Delete Document1 from Firestore - _print_to_console("Trying to delete Doucment1") - var del_task : FirestoreTask = _collection.delete("Document1") - _document = await del_task.delete_document - $delete_document.button_pressed = true - - # 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 = await query_task.task_finished - _print_to_console(result) - $run_query.button_pressed = true - - # If nothing has failed to this point, finish the test successfully - _print_to_console("\nFINISHED FIRESTORE TESTS") - _test_finished() + # Print to the console GUI that the test is starting + _print_to_console("\nSTARTING FIRESTORE TESTS") + + # Connect to the test collection + _print_to_console("\nConnecting to collection 'Firebasetester'") + _collection = Firebase.Firestore.collection('Firebasetester') + + # Connect to signals needed for testing + _collection.connect("add_document",Callable(self,"on_document_add")) + _collection.connect("get_document",Callable(self,"on_document_get")) + _collection.connect("update_document",Callable(self,"on_document_update")) + _collection.connect("delete_document",Callable(self,"on_document_delete")) + _collection.connect("error",Callable(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 = await add_task.add_document + $add_document.button_pressed = true + + # Get Document1 (Document that has been added from the previous step) + _print_to_console("Trying to get Document1") + var get_task = _collection.get_doc('Document1') + _document = await _collection.get_document + if(_document == null): + _test_error("Failed to get document") + return + else: + $get_document.button_pressed = true + + # Print Document1 to the console GUI + _print_to_console("Trying to print contents of Document1") + _print_to_console(_document) + $print_document.button_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 = await up_task.update_document + $update_document.button_pressed = true + + # Get Document1 (With updated that has been added from the previous step) + _print_to_console("Trying to get Document1") + _collection.get_doc('Document1') + _document = await _collection.get_document + $get_document_2.button_pressed = true + + # Print Document1 to the console GUI + _print_to_console("Trying to print contents of Document1") + _print_to_console(_document) + $print_document_2.button_pressed = true + + # Delete Document1 from Firestore + _print_to_console("Trying to delete Doucment1") + var del_task : FirestoreTask = _collection.delete("Document1") + _document = await del_task.delete_document + $delete_document.button_pressed = true + + # 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 = await query_task.task_finished + _print_to_console(result) + $run_query.button_pressed = true + + # 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") + _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") + _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") + _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") + _print_to_console("Document deleted successfully") # 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") + _print_to_console_error("error code: " + str(code)) + _print_to_console_error("message: " + str(message)) + _test_error("There was an issue") # 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 + 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 + 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") + get_tree().change_scene_to_file("res://main.tscn") From 569f730b5196861a6f1286086b1420038448a53f Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 25 May 2024 13:10:05 -0400 Subject: [PATCH 08/22] Big refactor changes --- .gitignore | 3 +- addons/godot-firebase/Utilies.gd | 23 -- addons/godot-firebase/Utilities.gd | 135 ++++++++ .../godot-firebase/auth/providers/google.gd | 18 +- addons/godot-firebase/database/database.gd | 23 +- .../godot-firebase/database/database_store.gd | 102 +++--- .../database/firebase_database_reference.tscn | 17 + .../firebase_once_database_reference.tscn | 16 + .../godot-firebase/database/once_reference.gd | 124 +++++++ addons/godot-firebase/database/reference.gd | 113 ++----- addons/godot-firebase/database/resource.gd | 6 +- .../dynamiclinks/dynamiclinks.gd | 118 +++---- addons/godot-firebase/firebase/firebase.gd | 20 +- addons/godot-firebase/firebase/firebase.tscn | 6 +- .../firestore/field_transform.gd | 22 ++ .../firestore/field_transform_array.gd | 35 ++ .../field_transforms/decrement_transform.gd | 19 ++ .../field_transforms/increment_transform.gd | 19 ++ .../field_transforms/max_transform.gd | 19 ++ .../field_transforms/min_transform.gd | 19 ++ .../server_timestamp_transform.gd | 10 + addons/godot-firebase/firestore/firestore.gd | 1 - .../firestore/firestore_collection.gd | 44 ++- .../firestore/firestore_document.gd | 69 ++-- .../firestore/firestore_task.gd | 15 +- .../firestore/firestore_transform.gd | 3 + .../godot-firebase/functions/function_task.gd | 24 +- addons/godot-firebase/functions/functions.gd | 4 +- .../queues/queueable_http_request.gd | 30 ++ .../queues/queueable_http_request.tscn | 6 + .../remote_config/firebase_remote_config.gd | 36 ++ .../remote_config/firebase_remote_config.tscn | 7 + .../remote_config/remote_config.gd | 14 + main.gd | 3 + main.tscn | 5 + tests/database/database.gd | 27 +- tests/firestore/firestore.gd | 71 ++-- tests/firestore/firestore.tscn | 175 ++++------ tests/remote_config/remote_config.gd | 107 ++++++ tests/remote_config/remote_config.tscn | 114 +++++++ tests/storage/storage.gd | 312 +++++++++--------- 41 files changed, 1328 insertions(+), 606 deletions(-) delete mode 100644 addons/godot-firebase/Utilies.gd create mode 100644 addons/godot-firebase/Utilities.gd create mode 100644 addons/godot-firebase/database/firebase_database_reference.tscn create mode 100644 addons/godot-firebase/database/firebase_once_database_reference.tscn create mode 100644 addons/godot-firebase/database/once_reference.gd create mode 100644 addons/godot-firebase/firestore/field_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transform_array.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/decrement_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/increment_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/max_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/min_transform.gd create mode 100644 addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd create mode 100644 addons/godot-firebase/firestore/firestore_transform.gd create mode 100644 addons/godot-firebase/queues/queueable_http_request.gd create mode 100644 addons/godot-firebase/queues/queueable_http_request.tscn create mode 100644 addons/godot-firebase/remote_config/firebase_remote_config.gd create mode 100644 addons/godot-firebase/remote_config/firebase_remote_config.tscn create mode 100644 addons/godot-firebase/remote_config/remote_config.gd create mode 100644 tests/remote_config/remote_config.gd create mode 100644 tests/remote_config/remote_config.tscn diff --git a/.gitignore b/.gitignore index 23589f4..2e81295 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ export_presets.cfg data_*/ addons/godot-firebase/.env *.DS_Store -.godot/ \ No newline at end of file +.godot/ +*.tmp diff --git a/addons/godot-firebase/Utilies.gd b/addons/godot-firebase/Utilies.gd deleted file mode 100644 index 2b69d36..0000000 --- a/addons/godot-firebase/Utilies.gd +++ /dev/null @@ -1,23 +0,0 @@ -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 - - -# 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"] diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd new file mode 100644 index 0000000..bd6c26f --- /dev/null +++ b/addons/godot-firebase/Utilities.gd @@ -0,0 +1,135 @@ +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 + + +# 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 : 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 : 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 diff --git a/addons/godot-firebase/auth/providers/google.gd b/addons/godot-firebase/auth/providers/google.gd index 2ea88cf..152a5cc 100644 --- a/addons/godot-firebase/auth/providers/google.gd +++ b/addons/godot-firebase/auth/providers/google.gd @@ -2,12 +2,12 @@ class_name GoogleProvider extends AuthProvider 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" + 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/database/database.gd b/addons/godot-firebase/database/database.gd index 755d4e4..3391518 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -16,7 +16,6 @@ func _set_config(config_json : Dictionary) -> void: _config = config_json _check_emulating() - func _check_emulating() -> void : ## Check emulating if not Firebase.emulating: @@ -28,8 +27,6 @@ func _check_emulating() -> void : else: _base_url = "http://localhost" - - func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: _auth = auth_result @@ -40,19 +37,15 @@ func _on_FirebaseAuth_logout() -> void: _auth = {} func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: - var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() - var getter := HTTPRequest.new() - getter.use_threads = true - var pusher : HTTPRequest = HTTPRequest.new() - pusher.use_threads = true - var listener : Node = Node.new() - listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) - var store : FirebaseDatabaseStore = FirebaseDatabaseStore.new() + 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) - firebase_reference.set_pusher(pusher) - firebase_reference.set_getter(getter) - firebase_reference.set_listener(listener) - firebase_reference.set_store(store) add_child(firebase_reference) return firebase_reference diff --git a/addons/godot-firebase/database/database_store.gd b/addons/godot-firebase/database/database_store.gd index e6ea028..a407241 100644 --- a/addons/godot-firebase/database/database_store.gd +++ b/addons/godot-firebase/database/database_store.gd @@ -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 = PackedStringArray([_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_at(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/firebase_database_reference.tscn b/addons/godot-firebase/database/firebase_database_reference.tscn new file mode 100644 index 0000000..27abf89 --- /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" 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" path="res://addons/http-sse-client/HTTPSSEClient.gd" id="2_4l0io"] +[ext_resource type="Script" 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..ac816e4 --- /dev/null +++ b/addons/godot-firebase/database/once_reference.gd @@ -0,0 +1,124 @@ +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 + +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 = [] + +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: + var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() + _oncer.request(ref_pos, _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 "" + + var filter = "" + + if _filter_query.has(ORDER_BY): + filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote + _filter_query.erase(ORDER_BY) + else: + 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(): + filter += _filter_tag + key + _equal_tag + _filter_query[key] + + return 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: + 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) -> 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()) + _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) diff --git a/addons/godot-firebase/database/reference.gd b/addons/godot-firebase/database/reference.gd index 1dacd0b..0429b92 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -24,19 +24,15 @@ const START_AT : String = "startAt" const END_AT : String = "endAt" const EQUAL_TO : String = "equalTo" -var _pusher : HTTPRequest -var _getter : 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 _get_queue : Array = [] -var _update_queue : Array = [] -var _delete_queue : Array = [] var _can_connect_to_host : bool = false const _put_tag : String = "put" @@ -56,6 +52,21 @@ const _key_filter_tag : String = "$key" 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 @@ -64,31 +75,6 @@ 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.request_completed.connect(on_push_request_complete) - -func set_getter(getter_ref : HTTPRequest) -> void: - if !_getter: - _getter = getter_ref - add_child(_getter) - _getter.request_completed.connect(on_get_request_complete) - - -func set_listener(listener_ref : Node) -> void: - if !_listener: - _listener = listener_ref - add_child(_listener) - _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) - func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) -> void: if data: var command = event @@ -104,12 +90,6 @@ func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) - patch_data_update.emit(FirebaseResource.new(data.path, data.data)) elif command == _delete_tag: delete_data_update.emit(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) @@ -118,36 +98,16 @@ func update(path : String, data : Dictionary) -> void: path = "" var to_update = JSON.stringify(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, HTTPClient.METHOD_PATCH, to_update) - else: - _update_queue.append({"path": path, "data": data}) + + var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) + _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) func push(data : Dictionary) -> void: var to_push = JSON.stringify(data) - if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) - else: - _push_queue.append(data) + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) 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, HTTPClient.METHOD_DELETE, "") - else: - _delete_queue.append(reference) - -# -# Gets a data snapshot once at the position passed in -# -func once(reference : String) -> void: - if _getter.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() - _getter.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") - else: - _get_queue.append(reference) + _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") # # Returns a deep copy of the current local copy of the data stored at this reference in the Firebase @@ -214,28 +174,3 @@ func on_push_request_complete(result : int, response_code : int, headers : Packe push_successful.emit() else: push_failed.emit() - - # handle queued operations - 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()) - -func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: - if response_code == HTTPClient.RESPONSE_OK: - var bod = Utilities.get_json_data(body) - once_successful.emit(bod) - else: - once_failed.emit() - - # handle queued operations - if _get_queue.size() > 0: - once(_get_queue.pop_front()) - diff --git a/addons/godot-firebase/database/resource.gd b/addons/godot-firebase/database/resource.gd index ff5b985..ff8a620 100644 --- a/addons/godot-firebase/database/resource.gd +++ b/addons/godot-firebase/database/resource.gd @@ -9,8 +9,8 @@ var key : String var data func _init(key : String,data): - self.key = key.lstrip("/") - self.data = 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/dynamiclinks/dynamiclinks.gd b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd index 67a1374..8f7a6c7 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -25,85 +25,85 @@ var _request_list_node : HTTPRequest 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() - Utilities.fix_http_request(_request_list_node) - _request_list_node.request_completed.connect(_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 == "": - 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)) + 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 + 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/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index efbaa38..f2eed9f 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -1,11 +1,12 @@ ## @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 @@ -17,27 +18,31 @@ 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 + +## @type FirebaseRemoteConfig +## The Firebase Remote Config API +@onready var RemoteConfigAPI := $RemoteConfig @export var emulating : bool = false @@ -130,7 +135,6 @@ func _setup_modules() -> void: Auth.token_refresh_succeeded.connect(module._on_FirebaseAuth_token_refresh_succeeded) Auth.logged_out.connect(module._on_FirebaseAuth_logout) - # ------------- func _printerr(error : String) -> void: diff --git a/addons/godot-firebase/firebase/firebase.tscn b/addons/godot-firebase/firebase/firebase.tscn index d5b34d7..31f5b56 100644 --- a/addons/godot-firebase/firebase/firebase.tscn +++ b/addons/godot-firebase/firebase/firebase.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=8 format=3 uid="uid://cvb26atjckwlq"] +[gd_scene load_steps=9 format=3 uid="uid://cvb26atjckwlq"] [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"] @@ -7,6 +7,7 @@ [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"] script = ExtResource("3") @@ -30,3 +31,6 @@ script = ExtResource("6") [node name="Functions" type="Node" parent="."] 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_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_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/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/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/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/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/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 5dd2e9b..979006d 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -114,7 +114,6 @@ func collection(path : String) -> FirestoreCollection: coll._config = _config coll.auth = auth coll.collection_name = path - coll.firestore = self collections[path] = coll return coll else: diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index e079f92..56d8bc0 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -10,7 +10,8 @@ extends RefCounted signal add_document(doc) signal get_document(doc) signal update_document(doc) -signal delete_document() +signal commit_document(result) +signal delete_document(deleted) signal error(code,status,message) const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " @@ -21,7 +22,6 @@ 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 @@ -64,7 +64,7 @@ func add(document_id : String, fields : Dictionary = {}) -> FirestoreTask: ## @args document_id, fields ## @arg-defaults , {} ## @return FirestoreTask -# used to UPDATE a document, specify @documentID and @fields +# used to UPDATE a document, specify @documentID, @fields func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_PATCH @@ -72,11 +72,36 @@ func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: 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("&") - + + for key in fields.keys(): + if fields[key] == null: + fields.erase(key) + task.update_document.connect(_on_update_document) task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) + var body = FirestoreDocument.dict2fields(fields) + + _process_request(task, document_id, url, JSON.stringify(body)) + return task + +func commit(document : FirestoreDocument) -> FirestoreTask: + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_COMMIT + var url = _base_url + _extended_url.rstrip("/") + ":commit" + task.commit_document.connect(_on_commit_document) + task.task_finished.connect(_on_task_finished.bind(document.doc_name), CONNECT_DEFERRED) + + 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() + _process_request(task, document.doc_name, url, JSON.stringify(body)) return task ## @args document_id @@ -118,7 +143,7 @@ func _process_request(task : FirestoreTask, document_id : String, url : String, _request_queues[document_id].append(task) else: _request_queues[document_id] = [] - firestore._pooled_request(task) + Firebase.Firestore._pooled_request(task) func _on_task_finished(task : FirestoreTask, document_id : String) -> void: if not _request_queues[document_id].is_empty(): @@ -134,9 +159,12 @@ func _on_add_document(document : FirestoreDocument): func _on_update_document(document : FirestoreDocument): update_document.emit(document) -func _on_delete_document(): - delete_document.emit() +func _on_delete_document(deleted): + delete_document.emit(deleted) func _on_error(code, status, message, task): error.emit(code, status, message) Firebase._printerr(message) + +func _on_commit_document(result): + commit_document.emit(result) diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index 8ec075b..f31520f 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -15,20 +15,25 @@ var document : Dictionary # the Document itself var doc_fields : Dictionary # only .fields var doc_name : String # only .name var create_time : String # createTime +var _transforms : FieldTransformArray # The transforms to apply func _init(doc : Dictionary = {},_doc_name : String = "",_doc_fields : Dictionary = {}): - 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) + _transforms = FieldTransformArray.new() + + document = doc + doc_name = doc.name + if doc_name.count("/") > 2: + doc_name = (doc_name.split("/") as Array).back() + + doc_fields = fields2dict(self.document) + self.create_time = doc.createTime # 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 : Dictionary = {} + var fields = {} var var_type : String = "" for field in dict.keys(): var field_value = dict[field] @@ -38,6 +43,7 @@ static func dict2fields(dict : Dictionary) -> Dictionary: 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" @@ -54,35 +60,48 @@ static func dict2fields(dict : Dictionary) -> Dictionary: 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} + +func add_field_transform(transform : FieldTransform) -> void: + _transforms.push_back(transform) + +func remove_field(field_path : String) -> void: + if document.has(field_path): + document[field_path] = null + + if doc_fields.has(field_path): + doc_fields[field_path] = null # Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' } -static func fields2dict(doc : Dictionary) -> Dictionary: - var dict : Dictionary = {} +static func fields2dict(doc) -> Dictionary: + var dict = {} 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"): + var fields = doc["fields"] + print(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] = (doc.fields)[field].values()[0] + dict[field] = fields[field].values()[0] return dict # Pass an Array to parse it to a Firebase arrayValue @@ -104,7 +123,7 @@ static func array2fields(array : Array) -> Array: 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 diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 7acc032..8fd659f 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -33,6 +33,9 @@ signal get_document(doc) ## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. ## @arg-types FirestoreDocument signal update_document(doc) +## Emitted when a [code]write(document)[/code] request for a document is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]result[/code] will be passed as a result. +## @arg-types FirestoreDocument +signal commit_document(result) ## Emitted when a [code]delete(document)[/code] request checked a [class FirebaseCollection] is successfully completed and [code]true[/code] will be passed. [code]error()[/code] signal will be emitted otherwise and [code]false[/code] will be passed as a result. ## @arg-types bool signal delete_document(success) @@ -52,7 +55,8 @@ enum Task { 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_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. @@ -62,7 +66,8 @@ const TASK_MAP = { Task.TASK_PATCH: "UPDATE DOCUMENT", Task.TASK_DELETE: "DELETE DOCUMENT", Task.TASK_QUERY: "QUERY COLLECTION", - Task.TASK_LIST: "LIST DOCUMENTS" + Task.TASK_LIST: "LIST DOCUMENTS", + Task.TASK_COMMIT: "COMMIT DOCUMENT" } ## The code indicating the request Firestore is processing. @@ -123,6 +128,8 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt if bod.has("nextPageToken"): data.append(bod.nextPageToken) listed_documents.emit(data) + Task.TASK_COMMIT: + commit_document.emit(bod) else: var description = "" if TASK_MAP.has(action): @@ -145,6 +152,8 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt Task.TASK_LIST: data = [] listed_documents.emit(data) + Task.TASK_COMMIT: + commit_document.emit(null) task_finished.emit(self) @@ -172,6 +181,8 @@ func set_action(value : int) -> void: _method = HTTPClient.METHOD_PATCH Task.TASK_DELETE: _method = HTTPClient.METHOD_DELETE + Task.TASK_COMMIT: + _method = HTTPClient.METHOD_POST func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: 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/functions/function_task.gd b/addons/godot-firebase/functions/function_task.gd index ba35b09..96a6222 100644 --- a/addons/godot-firebase/functions/function_task.gd +++ b/addons/godot-firebase/functions/function_task.gd @@ -42,18 +42,18 @@ var _fields : String = "" var _headers : PackedStringArray = [] 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 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: - function_executed.emit(result, data) - else: - error = {result=result, response_code=response_code, data=data} - task_error.emit(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)) - task_finished.emit(data) + task_finished.emit(data) diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index d1d9586..605a180 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -84,7 +84,6 @@ func execute(function: String, method: int, params: Dictionary = {}, body: Dicti url += key + "=" + params[key] + "&" if not body.is_empty(): - function_task._headers = PackedStringArray(["Content-Type: application/json"]) function_task._fields = JSON.stringify(body) _pooled_request(function_task) @@ -158,7 +157,7 @@ func _pooled_request(task : FunctionTask) -> void: 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: @@ -169,6 +168,7 @@ func _pooled_request(task : FunctionTask) -> void: 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)) 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.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.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/main.gd b/main.gd index 9c0f259..293408e 100644 --- a/main.gd +++ b/main.gd @@ -21,3 +21,6 @@ func _on_storage_tests_pressed(): func _on_dynamiclinks_tests_pressed(): 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.tscn b/main.tscn index a9fc54b..e68bfd2 100644 --- a/main.tscn +++ b/main.tscn @@ -77,6 +77,10 @@ label = "Storage" layout_mode = 2 label = "Database" +[node name="remoteconfig_tests" parent="menu/mid/mid2/HBoxContainer2" instance=ExtResource("4")] +layout_mode = 2 +label = "RemoteConfig" + [node name="bot" type="VBoxContainer" parent="menu"] layout_mode = 2 size_flags_vertical = 3 @@ -94,3 +98,4 @@ uppercase = true [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/tests/database/database.gd b/tests/database/database.gd index ca4f3af..b79333b 100644 --- a/tests/database/database.gd +++ b/tests/database/database.gd @@ -65,6 +65,7 @@ func _test_database(): # Get the database reference that we will be working with _print_to_console("\nGetting the Databse RefCounted...") database_reference = Firebase.Database.get_database_reference("FirebaseTester/data", { }) + 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 @@ -73,23 +74,31 @@ func _test_database(): 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) - database_reference.connect("once_failed", _on_once_failed) - database_reference.connect("once_successful", _on_once_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.button_pressed = true await data_key_ready # Get data once from the RTDB _print_to_console("\n\nAttempting a once-off get from the RTD") - database_reference.once(added_data_key + "/user_name") + once_database_reference.once(added_data_key + "/user_name") + await once_database_reference.once_failed + _print_to_console("Once failed") $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!'}) + + _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 @@ -126,6 +135,16 @@ func _on_push_failed(): 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(): diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index 647ca4c..83bca43 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -43,7 +43,7 @@ func _test_error(data) -> void: func _cleanup_previous_run(): var del_task : FirestoreTask = _collection.delete("Document1") - _document = await del_task.delete_document + var deleted = await del_task.delete_document # Function called when login to Firebase has completed successfully func _on_FirebaseAuth_login_succeeded(_auth) -> void: @@ -74,15 +74,15 @@ func _test_firestore() -> void: _collection = Firebase.Firestore.collection('Firebasetester') # Connect to signals needed for testing - _collection.connect("add_document",Callable(self,"on_document_add")) - _collection.connect("get_document",Callable(self,"on_document_get")) - _collection.connect("update_document",Callable(self,"on_document_update")) - _collection.connect("delete_document",Callable(self,"on_document_delete")) - _collection.connect("error",Callable(self,"on_document_error")) + _collection.add_document.connect(on_document_add) + _collection.get_document.connect(on_document_get) + _collection.update_document.connect(on_document_update) + _collection.delete_document.connect(on_document_delete) + _collection.error.connect(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'}) + var add_task : FirestoreTask = _collection.add("Document1", {'name': 'Document1', 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) _document = await add_task.add_document $add_document.button_pressed = true @@ -101,27 +101,51 @@ func _test_firestore() -> void: _print_to_console(_document) $print_document.button_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 = await up_task.update_document - $update_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 up_task = _collection.commit(_document) + var result = await up_task.commit_document - # Get Document1 (With updated that has been added from the previous step) - _print_to_console("Trying to get Document1") - _collection.get_doc('Document1') + get_task = _collection.get_doc('Document1') _document = await _collection.get_document - $get_document_2.button_pressed = true - # Print Document1 to the console GUI - _print_to_console("Trying to print contents of 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.button_pressed = true + _print_to_console("Attempting to remove item from Document1") + var key = _document.document.keys().front() + _document.remove_field(key) + + _print_to_console("Key erased: " + key) + _print_to_console("Document now:\n" + str(_document)) + _print_to_console("\n") + + var previous_document_size = _document.doc_fields.keys().size() + + var delete_from_task : FirestoreTask = _collection.update("Document1", _document.doc_fields) + _document = await delete_from_task.update_document + + _print_to_console("Document now:\n" + str(_document)) + if previous_document_size < _document.doc_fields.keys().size(): + _print_to_console_error("Did not properly delete item, it was merged after only fixing document and not doc_fields") + + _print_to_console("Previous document keys count: " + str(previous_document_size)) + _print_to_console("Document keys count: " + str(_document.doc_fields.keys().size())) + # Delete Document1 from Firestore - _print_to_console("Trying to delete Doucment1") + _print_to_console("Trying to delete Document1") var del_task : FirestoreTask = _collection.delete("Document1") - _document = await del_task.delete_document + var deleted = await del_task.delete_document $delete_document.button_pressed = true # Query Collection @@ -132,7 +156,7 @@ func _test_firestore() -> void: query.order_by("points", FirestoreQuery.DIRECTION.DESCENDING) query.limit(10) var query_task : FirestoreTask = Firebase.Firestore.query(query) - var result = await query_task.task_finished + result = await query_task.result_query _print_to_console(result) $run_query.button_pressed = true @@ -150,11 +174,12 @@ func on_document_get(document_got : FirestoreDocument) -> void: # Function called when a document has been updated in Firestore successfully func on_document_update(document_updated : FirestoreDocument) -> void: + _print_to_console("Current document after update: " + str(document_updated)) _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 on_document_delete(deleted) -> void: + _print_to_console("Document deleted: " + str(deleted)) # Function called when a function with Firestore has failed func on_document_error(code, status, message) -> void: diff --git a/tests/firestore/firestore.tscn b/tests/firestore/firestore.tscn index ba5c392..321b2ca 100644 --- a/tests/firestore/firestore.tscn +++ b/tests/firestore/firestore.tscn @@ -1,76 +1,55 @@ -[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="Texture2D" id=2] -[ext_resource path="res://assets/background.png" type="Texture2D" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" 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="Texture2D" id=6] -[ext_resource path="res://assets/buttons/normal_button.gd" type="Script" id=7] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="FontFile" id=8] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="FontFile" id=9] - - -[sub_resource type="FontFile" id=1] -size = 23 -font_data = ExtResource( 8 ) +[ext_resource type="Script" 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" path="res://assets/buttons/normal_button.gd" id="7"] [node name="firestore" type="Node2D"] -script = ExtResource( 1 ) +script = ExtResource("1") [node name="background" type="TextureRect" parent="."] offset_left = 5.59372 offset_top = 2.00226 offset_right = 1029.59 offset_bottom = 602.002 -texture = ExtResource( 3 ) -__meta__ = { -"_edit_use_anchors_": false -} +texture = ExtResource("3") [node name="title" type="Label" parent="."] offset_left = 376.0 offset_top = 11.8108 offset_right = 557.0 offset_bottom = 55.8108 -custom_fonts/font = ExtResource( 4 ) text = "Firestore Tests" -align = 1 +horizontal_alignment = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 5 )] +[node name="back" parent="." instance=ExtResource("5")] offset_right = 190.354 offset_bottom = 44.6465 label = "Back" [node name="test_firestore" type="TextureButton" parent="."] +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 offset_top = 65.0 offset_right = 190.0 offset_bottom = 110.0 -texture_normal = ExtResource( 2 ) -texture_disabled = ExtResource( 6 ) -script = ExtResource( 7 ) -__meta__ = { -"_edit_use_anchors_": false -} +texture_normal = ExtResource("2") +texture_disabled = ExtResource("6") +script = ExtResource("7") label = "Test Firestore" [node name="label" type="RichTextLabel" parent="test_firestore"] +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="."] offset_left = 249.251 @@ -79,13 +58,8 @@ 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", -]] +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 131.0 offset_right = 34.0 @@ -93,21 +67,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Login" -[node name="add_document" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="add_document" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 155.0 offset_right = 34.0 @@ -115,24 +84,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Add Document" -__meta__ = { -"_edit_use_anchors_": false -} -[node name="get_document" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="get_document" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 180.0 offset_right = 34.0 @@ -140,21 +101,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Get Document" -[node name="print_document" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="print_document" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 203.0 offset_right = 34.0 @@ -162,21 +118,16 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Print Document" -[node name="update_document" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="update_document" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 offset_top = 224.0 offset_right = 34.0 @@ -184,104 +135,100 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Update Document" -[node name="get_document_2" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="delete_item_from_document" type="CheckBox" parent="." groups=["tests"]] +auto_translate_mode = 1 +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"]] offset_left = 10.0 -offset_top = 246.0 +offset_top = 275.0 offset_right = 34.0 -offset_bottom = 270.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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Get Document" -[node name="print_document_2" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="print_document_2" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 -offset_top = 266.0 +offset_top = 295.0 offset_right = 34.0 -offset_bottom = 290.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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Print Document" -[node name="delete_document" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="delete_document" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 -offset_top = 285.0 +offset_top = 314.0 offset_right = 34.0 -offset_bottom = 309.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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Delete Document" -[node name="run_query" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="run_query" type="CheckBox" parent="." groups=["tests"]] offset_left = 10.0 -offset_top = 309.0 +offset_top = 338.0 offset_right = 34.0 -offset_bottom = 333.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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Run Query" [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.tscn b/tests/remote_config/remote_config.tscn new file mode 100644 index 0000000..b78f9e2 --- /dev/null +++ b/tests/remote_config/remote_config.tscn @@ -0,0 +1,114 @@ +[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 +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 c8e3ed0..3557de8 100644 --- a/tests/storage/storage.gd +++ b/tests/storage/storage.gd @@ -12,197 +12,197 @@ const _password : String = 'Password1234' # Function called when the scene is ready func _ready(): - Firebase.Auth.connect("login_succeeded",Callable(self,"_on_FirebaseAuth_login_succeeded")) - Firebase.Auth.connect("login_failed",Callable(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 # 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_storage.disabled = true - $image.texture = null + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_storage.disabled = true + $image.texture = null # 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_storage.disabled = false + _test_running = false + $back.disabled = false + $test_storage.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_storage() + _print_to_console("Login with email and password has worked") + $login_check.button_pressed = true + _test_storage() # 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)) + _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 Storage' button # Starts the test func _on_test_storage_pressed(): - _test_started() - Firebase.Auth.login_with_email_and_password(_email, _password) + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) # Main function that is run when testing Storage func _test_storage(): - # Print to the console GUI that the test is starting - _print_to_console("STARTING STORAGE TESTS") - - # 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") - await upload_task.task_finished - $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') - var data = await image.task_finished - var converted_image = task2image(image) - $image.texture = converted_image - $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() - await url_task.task_finished - _print_to_console(url_task.data) - $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() - await meta_task.task_finished - _print_to_console(meta_task.data) - $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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - var delete_task = Firebase.Storage.ref("Firebasetester/upload/image.png").delete() - await delete_task.task_finished - _print_to_console("After Delete...") - list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - $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") - await upload_task.task_finished - $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() - await meta_task.task_finished - _print_to_console(meta_task.data) - $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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - delete_task = Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").delete() - await delete_task.task_finished - _print_to_console("After Delete...") - list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - $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", {}) - await upload_task.task_finished - $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"}) - await meta_task.task_finished - $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() - await meta_task.task_finished - _print_to_console(meta_task.data) - $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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - delete_task = Firebase.Storage.ref("Firebasetester/upload/junkdata").delete() - await delete_task.task_finished - _print_to_console("After Delete...") - list_all_task = Firebase.Storage.ref("Firebasetester").list_all() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - $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() + # Print to the console GUI that the test is starting + _print_to_console("STARTING STORAGE TESTS") + + # 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") + await upload_task.task_finished + $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') + var data = await image.task_finished + var converted_image = task2image(image) + $image.texture = converted_image + $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() + await url_task.task_finished + _print_to_console(url_task.data) + $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() + await meta_task.task_finished + _print_to_console(meta_task.data) + $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() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + var delete_task = Firebase.Storage.ref("Firebasetester/upload/image.png").delete() + await delete_task.task_finished + _print_to_console("After Delete...") + list_all_task = Firebase.Storage.ref("Firebasetester").list_all() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + $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") + await upload_task.task_finished + $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() + await meta_task.task_finished + _print_to_console(meta_task.data) + $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() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + delete_task = Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").delete() + await delete_task.task_finished + _print_to_console("After Delete...") + list_all_task = Firebase.Storage.ref("Firebasetester").list_all() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + $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", {}) + await upload_task.task_finished + $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"}) + await meta_task.task_finished + $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() + await meta_task.task_finished + _print_to_console(meta_task.data) + $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() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + delete_task = Firebase.Storage.ref("Firebasetester/upload/junkdata").delete() + await delete_task.task_finished + _print_to_console("After Delete...") + list_all_task = Firebase.Storage.ref("Firebasetester").list_all() + await list_all_task.task_finished + _print_to_console(list_all_task.data) + $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() # 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 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: - var new_image := Image.new() - match typeof(task.data): - TYPE_PACKED_BYTE_ARRAY: - var data : PackedByteArray = task.data - if data.size()>1: - 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) - "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) - var new_texture := ImageTexture.new() - new_texture.create_from_image(new_image) - return new_texture + var new_image := Image.new() + match typeof(task.data): + TYPE_PACKED_BYTE_ARRAY: + var data : PackedByteArray = task.data + if data.size()>1: + 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) + "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) + var new_texture := ImageTexture.new() + new_texture.create_from_image(new_image) + return new_texture # 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 + 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 + 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") + get_tree().change_scene_to_file("res://main.tscn") From f111636e3e5e413d751af990f7346170f81ba2b5 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sun, 2 Jun 2024 15:05:26 -0400 Subject: [PATCH 09/22] Update from main Firebase plugin --- addons/godot-firebase/Utilities.gd | 4 ++-- addons/godot-firebase/auth/auth.gd | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index bd6c26f..3000511 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -58,7 +58,7 @@ class SignalReducer extends RefCounted: # No need for a node, as this deals stri var awaiters : Array[Signal] = [] var reducers = { - 0 : completed.emit, + 0 : func(): completed.emit(), 1 : func(p): completed.emit(), 2 : func(p1, p2): completed.emit(), 3 : func(p1, p2, p3): completed.emit(), @@ -75,7 +75,7 @@ class SignalReducerWithResult extends RefCounted: # No need for a node, as this var awaiters : Array[Signal] = [] var reducers = { - 0 : completed.emit, + 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 }), diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 4b3a21c..ec6c24c 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -468,6 +468,7 @@ func _on_FirebaseAuth_request_completed(result : int, response_code : int, heade 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) -> bool: @@ -601,6 +602,8 @@ func delete_user_account() -> void: 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. From 35aee61423ecffd3a5da6b7975d15ebd34f85230 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Mon, 27 May 2024 07:39:41 -0400 Subject: [PATCH 10/22] Fix database tests slightly --- tests/database/database.gd | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/database/database.gd b/tests/database/database.gd index b79333b..cf9eaaa 100644 --- a/tests/database/database.gd +++ b/tests/database/database.gd @@ -90,7 +90,6 @@ func _test_database(): # 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") - await once_database_reference.once_failed _print_to_console("Once failed") $once_data_check.button_pressed = true # Update data in the RTDB From 925990333e45539d13d0e5d8125d5a33efb7a4a8 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Wed, 29 May 2024 08:17:48 -0400 Subject: [PATCH 11/22] Start switch to more nodes than RefCounted --- addons/godot-firebase/Utilities.gd | 162 ++++++++++++++++ addons/godot-firebase/firestore/firestore.gd | 2 +- .../firestore/firestore_collection.gd | 40 ++-- .../firestore/firestore_document.gd | 182 +++++------------- .../firestore/firestore_query.gd | 4 +- 5 files changed, 233 insertions(+), 157 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index 3000511..2eb2fcd 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -12,6 +12,168 @@ static func get_json_data(value): 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} + +static func from_firebase_type(value : Variant) -> Variant: + if value == null: + return null + + return value.values()[0] + +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"] + print(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() + 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: + 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. diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 979006d..2185301 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -115,6 +115,7 @@ func collection(path : String) -> FirestoreCollection: coll.auth = auth coll.collection_name = path collections[path] = coll + add_child(coll) return coll else: return collections[path] @@ -226,7 +227,6 @@ func _set_config(config_json : Dictionary) -> void: _check_emulating() - func _check_emulating() -> void : ## Check emulating if not Firebase.emulating: diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 56d8bc0..7be0db6 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -5,7 +5,7 @@ ## Documentation TODO. @tool class_name FirestoreCollection -extends RefCounted +extends Node signal add_document(doc) signal get_document(doc) @@ -50,7 +50,7 @@ func get_doc(document_id : String) -> FirestoreTask: ## @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: +func add(document_id : String, data : Dictionary = {}) -> FirestoreTask: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_POST task.data = collection_name + "/" + document_id @@ -58,41 +58,42 @@ func add(document_id : String, fields : Dictionary = {}) -> FirestoreTask: task.add_document.connect(_on_add_document) task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) + _process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data))) return task ## @args document_id, fields ## @arg-defaults , {} ## @return FirestoreTask # used to UPDATE a document, specify @documentID, @fields -func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: +func update(document : FirestoreDocument) -> 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(): + 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 fields.keys(): - if fields[key] == null: - fields.erase(key) + for key in document.keys(): + if document.get_value(key) == null: + document._erase(key) task.update_document.connect(_on_update_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - var body = FirestoreDocument.dict2fields(fields) + task.task_finished.connect(_on_task_finished.bind(document.doc_name), CONNECT_DEFERRED) + + var body = JSON.stringify({"fields": document.document}, " ") - _process_request(task, document_id, url, JSON.stringify(body)) + _process_request(task, document.doc_name, url, body) return task func commit(document : FirestoreDocument) -> FirestoreTask: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_COMMIT - var url = _base_url + _extended_url.rstrip("/") + ":commit" + var url = get_database_url("commit") task.commit_document.connect(_on_commit_document) task.task_finished.connect(_on_task_finished.bind(document.doc_name), CONNECT_DEFERRED) - + document._transforms.set_config( { "extended_url": _extended_url, @@ -154,6 +155,7 @@ func _on_get_document(document : FirestoreDocument): get_document.emit(document) func _on_add_document(document : FirestoreDocument): + document.collection_name = collection_name add_document.emit(document) func _on_update_document(document : FirestoreDocument): @@ -168,3 +170,11 @@ func _on_error(code, status, message, task): func _on_commit_document(result): commit_document.emit(result) + +func _add_document_listener(document_path : String, with_func : Callable): + var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate() + add_child(listener) + return listener.connect_to_firestore(_config) + +func get_database_url(append) -> String: + return _base_url + _extended_url.rstrip("/") + ":" + append diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index f31520f..4c9a4fb 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -12,169 +12,73 @@ extends RefCounted # 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 -func _init(doc : Dictionary = {},_doc_name : String = "",_doc_fields : Dictionary = {}): +func _init(doc : Dictionary = {}): _transforms = FieldTransformArray.new() - document = doc + document = doc.fields doc_name = doc.name if doc_name.count("/") > 2: doc_name = (doc_name.split("/") as Array).back() - doc_fields = fields2dict(self.document) - self.create_time = doc.createTime -# 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 "." 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} - func add_field_transform(transform : FieldTransform) -> void: _transforms.push_back(transform) func remove_field(field_path : String) -> void: if document.has(field_path): document[field_path] = null - - if doc_fields.has(field_path): - doc_fields[field_path] = null - -# 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"] - print(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 +func _erase(field_path : String) -> void: + document.erase(field_path) -# 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() - return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values +func add_or_update_field(field_path : String, value : Variant) -> void: + document[field_path] = Utilities.to_firebase_type(value) -# 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 : 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 +func on_snapshot(listener : Callable): + var collection = Firebase.Firestore.collection(collection_name) + var result = collection._add_document_listener(doc_name, listener) + return result -static func is_field_timestamp(field : Dictionary) -> bool: - return field.has_all(['year','month','day','hour','minute','second']) +func get_listener_request(config, _extended_url, _collection_name) -> Dictionary: + var _separator = "/" + return \ + { + "addTarget": { + "documents": [_extended_url + _collection_name + _separator + doc_name] + } + } # 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( + return ("doc_name: {doc_name}, \ndata: {data}, \ncreate_time: {create_time}\n").format( {doc_name = self.doc_name, - doc_fields = self.doc_fields, + data = document, create_time = self.create_time}) + +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 + +func _set(property: StringName, value: Variant) -> bool: + document[property] = Utilities.to_firebase_type(value) + return true + +func keys(): + return document.keys() diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index fc6d034..6e4b764 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -195,7 +195,7 @@ func limit(limit : int) -> FirestoreQuery: # UTILITIES ---------------------------------------- static func _cursor_object(value, before : bool) -> Cursor: - var parse : Dictionary = FirestoreDocument.dict2fields({value = value}).fields.value + 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 @@ -210,7 +210,7 @@ func create_field_filter(field : String, operator : int, value) -> Dictionary: fieldFilter = { field = { fieldPath = field }, op = OPERATOR.keys()[operator], - value = FirestoreDocument.dict2fields({value = value}).fields.value + value = Utilities.dict2fields({value = value}).fields.value } } func create_unary_filter(field : String, operator : int) -> Dictionary: From 2845d3584a69c1f20b0169a15b5bc48e2247d280 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Wed, 29 May 2024 08:18:07 -0400 Subject: [PATCH 12/22] Initial tests refactor, add listener --- .../firestore/firestore_listener.gd | 141 ++++++++++++ .../firestore/firestore_listener.tscn | 6 + tests/firestore/firestore.gd | 205 +++++++++++------- 3 files changed, 278 insertions(+), 74 deletions(-) create mode 100644 addons/godot-firebase/firestore/firestore_listener.gd create mode 100644 addons/godot-firebase/firestore/firestore_listener.tscn diff --git a/addons/godot-firebase/firestore/firestore_listener.gd b/addons/godot-firebase/firestore/firestore_listener.gd new file mode 100644 index 0000000..3d0eef8 --- /dev/null +++ b/addons/godot-firebase/firestore/firestore_listener.gd @@ -0,0 +1,141 @@ +class_name FirestoreListener +extends Node + +var http_client = HTTPClient.new() + +var project_id = "roundtable-5c241" +var collection_path = "Firebasetester" +var document_id = "Document1" +var connected = false +var api_key + +var response_body = PackedByteArray() + +var host + +var request_to_send : Callable +var request_sent := false + +func _ready(): + set_process(false) + +func connect_to_firestore(config): + project_id = config.projectId.uri_encode() + api_key = config.apiKey + host = "firestore.googleapis.com" + var port = 443 # Use port 443 for HTTPS + var tlsoptions = TLSOptions.client(null, "") + + var err = http_client.connect_to_host(host, port, tlsoptions) + if err != OK: + print("Failed to connect to host: ", err) + else: + print("Connected to host") + set_process(true) # Start polling + + var path = "/google.firestore.v1.Firestore/Listen/channel" + "?VER=8&database=projects%2Froundtable-5c241%2Fdatabases%2F(default)&RID=97627&CVER=22&X-HTTP-Session-Id=gsessionid&zx=cjl2twashomb&t=1" + var body = { + "addTarget": + { + "documents": + { + "documents": ["projects/%s/databases/(default)/documents/%s/%s" % [project_id, collection_path, document_id]] + } + } + } + + var encoded_body = encode_body(body) + encoded_body = "&req0___data__=" + encoded_body + + var headers = [ + "Authorization: Bearer %s" % Firebase.Auth.auth.idtoken, + "Content-Type: x-www-form-urlencoded", + "Content-Length: " + str(encoded_body.length()), + ":authority: firestore.googleapis.com", + ":method: POST", + ":path: /google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Froundtable-5c241%2Fdatabases%2F(default)&RID=97627&CVER=22&X-HTTP-Session-Id=gsessionid&zx=cjl2twashomb&t=1", + ":scheme: https", + ] + + print("Final path: ", path) + set_process(true) + request_to_send = func(): send_request(path, headers, encoded_body) + + return self + +func _process(delta: float) -> void: + var status = http_client.get_status() + if status in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING, HTTPClient.STATUS_REQUESTING]: + http_client.poll() + return + + if http_client.get_status() == HTTPClient.STATUS_BODY: + http_client.poll() + response_body += http_client.read_response_body_chunk() + OS.delay_msec(50) + + if http_client.has_response(): + var response_code = http_client.get_response_code() + var response_headers = http_client.get_response_headers_as_dictionary() + var response_body = PackedByteArray() + + if response_code == 200: + print("Response code: ", response_code) + print("Response headers: ", response_headers) + if response_body.size() == 0: + print("Request returned size zero") + else: + var response_json = Utilities.get_json_data(response_body) + if response_json.error == OK: + var response_data = response_json.result + print("Request successful: %s" % response_data) + else: + print("JSON parse error: %s" % response_json.error_string) + else: + print("Request failed with status code: %s" % response_code) + print("Response body: %s" % Utilities.get_json_data(response_body)) + + return + + if status != HTTPClient.STATUS_CONNECTED: + print("Failed to connect") + set_process(false) + return + + if not request_sent: + request_to_send.call() + request_sent = true + +func send_request(url, headers, body): + http_client.request(HTTPClient.METHOD_POST, url, headers, body) + +func encode_body(body : Dictionary) -> String: + var query_string = [] + for key in body.keys(): + var value = body[key] + if typeof(value) == TYPE_DICTIONARY: + value = JSON.stringify(value) + + value = value.replace("\"", "") + key = key.replace("\"", "") + query_string.append(key.uri_encode() + "=".uri_encode() + value.uri_encode()) + return "&".join(query_string) + +func handle_response(chunk): + var response_text = chunk.get_string_from_utf8() + print("Received chunk: ", response_text) + var response_json = Utilities.get_json_data(response_text) + if response_json.error == OK: + print("Parsed JSON: ", response_json.result) + else: + print("Failed to parse JSON: ", response_json.error_string()) + +class FirestoreListenerConnection extends RefCounted: + var connection + + func _init(connection_node): + connection = connection_node + + func stop(): + connection.tcp_client.disconnect_from_host() + connection.free() diff --git a/addons/godot-firebase/firestore/firestore_listener.tscn b/addons/godot-firebase/firestore/firestore_listener.tscn new file mode 100644 index 0000000..9f5e246 --- /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" path="res://addons/godot-firebase/firestore/firestore_listener.gd" id="1_qlaei"] + +[node name="FirestoreListener" type="Node"] +script = ExtResource("1_qlaei") diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index 83bca43..ff4421b 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -12,10 +12,12 @@ var _document : FirestoreDocument const _email : String = 'testaccount@godotnuts.test' const _password : String = 'Password1234' +var listener_test_count := 0 + # Function called when the scene is ready func _ready(): - Firebase.Auth.connect("login_succeeded",Callable(self,"_on_FirebaseAuth_login_succeeded")) - Firebase.Auth.connect("login_failed",Callable(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 @@ -79,90 +81,145 @@ func _test_firestore() -> void: _collection.update_document.connect(on_document_update) _collection.delete_document.connect(on_document_delete) _collection.error.connect(on_document_error) - await _cleanup_previous_run() + #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', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) - _document = await add_task.add_document - $add_document.button_pressed = true - - # Get Document1 (Document that has been added from the previous step) - _print_to_console("Trying to get Document1") + #_print_to_console("Trying to add a document") + #var add_task : FirestoreTask = _collection.add("Document1", {'name': 'Document1', 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) + #_document = await add_task.add_document + #$add_document.button_pressed = true + # + ## Get Document1 (Document that has been added from the previous step) + #_print_to_console("Trying to get Document1") var get_task = _collection.get_doc('Document1') _document = await _collection.get_document - if(_document == null): - _test_error("Failed to get document") - return - else: - $get_document.button_pressed = true - - # Print Document1 to the console GUI - _print_to_console("Trying to print contents of Document1") - _print_to_console(_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 up_task = _collection.commit(_document) - var result = await up_task.commit_document - - get_task = _collection.get_doc('Document1') + #if(_document == null): + #_test_error("Failed to get document") + #return + #else: + #$get_document.button_pressed = true + # + ## Print Document1 to the console GUI + #_print_to_console("Trying to print contents of Document1") + #_print_to_console(_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 up_task = _collection.commit(_document) + #var result = await up_task.commit_document + # + #get_task = _collection.get_doc('Document1') + #_document = await _collection.get_document + # + ### Print Document1 to the console GUI + #_print_to_console("Trying to print contents of Document1 after transform") + #_print_to_console(_document) + #$print_document_2.button_pressed = true + # + #_print_to_console("Attempting to remove item from Document1") + #var key = "name" + #_document.remove_field(key) + # + #_print_to_console("Key erased: " + key) + #_print_to_console("Document now:\n" + str(_document)) + #_print_to_console("\n") + # + #var previous_document_size = _document.keys().size() + # + #var delete_item_from_document_task : FirestoreTask = _collection.update(_document) + #await delete_item_from_document_task.update_document + # + #get_task = _collection.get_doc('Document1') + #_document = await _collection.get_document + + #_print_to_console("Document now:\n" + str(_document)) + #if previous_document_size == _document.keys().size(): + #_print_to_console_error("Did not properly delete item from document") + # + #_print_to_console("Previous document keys count: " + str(previous_document_size)) + #_print_to_console("Document keys count: " + str(_document.keys().size())) + # + ## Delete Document1 from Firestore + #_print_to_console("Trying to delete Document1") + #var del_task : FirestoreTask = _collection.delete("Document1") + #var deleted = await del_task.delete_document + #$delete_document.button_pressed = true + # + ## 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) + #result = await query_task.result_query + #_print_to_console(result) + #$run_query.button_pressed = true + + _print_to_console("Running listener tests") + #await _cleanup_previous_run() + + await run_listener_tests() + + # If nothing has failed to this point, finish the test successfully + _print_to_console("\nFINISHED FIRESTORE TESTS") + _test_finished() + +func run_listener_tests() -> void: + #var add_task : FirestoreTask = _collection.add("Document1", {'name': 'Document1', 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) + #_document = await add_task.add_document + listener_test_count = 0 + var listener = _document.on_snapshot(func(result): print(result)) + await get_tree().create_timer(5.0).timeout + + const new_doc_name = 'NewDocument' + + _document.add_or_update_field("name", new_doc_name) + + var update_task : FirestoreTask = _collection.update(_document) + _document = await update_task.update_document + # + #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 up_task = _collection.commit(_document) + #var result = await up_task.commit_document + # + # + await get_tree().create_timer(10.0).timeout + var get_task = _collection.get_doc('Document1') _document = await _collection.get_document - ## Print Document1 to the console GUI - _print_to_console("Trying to print contents of Document1 after transform") - _print_to_console(_document) - $print_document_2.button_pressed = true - _print_to_console("Attempting to remove item from Document1") - var key = _document.document.keys().front() - _document.remove_field(key) + _print_to_console("Count outside: " + str(listener_test_count)) - _print_to_console("Key erased: " + key) - _print_to_console("Document now:\n" + str(_document)) - _print_to_console("\n") + #listener.stop() - var previous_document_size = _document.doc_fields.keys().size() - var delete_from_task : FirestoreTask = _collection.update("Document1", _document.doc_fields) - _document = await delete_from_task.update_document + _document.add_or_update_field("name", new_doc_name + "2") - _print_to_console("Document now:\n" + str(_document)) - if previous_document_size < _document.doc_fields.keys().size(): - _print_to_console_error("Did not properly delete item, it was merged after only fixing document and not doc_fields") + update_task = _collection.update(_document) + _document = await update_task.update_document - _print_to_console("Previous document keys count: " + str(previous_document_size)) - _print_to_console("Document keys count: " + str(_document.doc_fields.keys().size())) + _print_to_console("Count: " + str(listener_test_count)) - # Delete Document1 from Firestore - _print_to_console("Trying to delete Document1") - var del_task : FirestoreTask = _collection.delete("Document1") - var deleted = await del_task.delete_document - $delete_document.button_pressed = true - - # 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) - result = await query_task.result_query - _print_to_console(result) - $run_query.button_pressed = true - # 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: @@ -174,7 +231,7 @@ func on_document_get(document_got : FirestoreDocument) -> void: # Function called when a document has been updated in Firestore successfully func on_document_update(document_updated : FirestoreDocument) -> void: - _print_to_console("Current document after update: " + str(document_updated)) + #_print_to_console("Current document after update: " + str(document_updated)) _print_to_console("Document Updated successfully") # Function called when a document has been deleted in Firestore successfully From 63a36cdaffaae30666274d8e46b037450c3682ac Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sun, 2 Jun 2024 15:01:49 -0400 Subject: [PATCH 13/22] Big refactor part 2 --- addons/godot-firebase/Utilities.gd | 45 ++- addons/godot-firebase/firestore/firestore.gd | 164 ++++------- .../firestore/firestore_collection.gd | 149 +++++----- .../firestore/firestore_document.gd | 56 ++-- .../firestore/firestore_listener.gd | 275 ++++++++++-------- .../firestore/firestore_task.gd | 105 ++----- addons/godot-firebase/functions/functions.gd | 3 +- tests/firestore/firestore.gd | 127 +++----- 8 files changed, 436 insertions(+), 488 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index 2eb2fcd..fdf1095 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -55,8 +55,26 @@ static func dict2fields(dict : Dictionary) -> Dictionary: static func from_firebase_type(value : Variant) -> Variant: if value == null: return null - - return value.values()[0] + + if value.has("mapValue"): + value = _from_firebase_type_recursive(value.value()[0].fields) + elif value.has("timestampValue"): + value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) + else: + value = value.values()[0] + + return value + +static func _from_firebase_type_recursive(value : Variant) -> Variant: + if value == null: + return null + + if value.has("mapValue") or value.has("timestampValue"): + value = _from_firebase_type_recursive(value.value()[0].fields) + else: + value = value.value()[0] + + return value static func to_firebase_type(value : Variant) -> Dictionary: var var_type : String = "" @@ -156,19 +174,22 @@ static func fields2array(array : Dictionary) -> Array: # 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() - return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values + #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: - 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 + 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']) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 2185301..772a228 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -18,16 +18,8 @@ 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. @@ -58,10 +50,6 @@ var persistence_enabled : bool = false ## @default true var networking: bool = true : set = set_networking -## A Dictionary containing all collections currently referenced. -## @type Dictionary -var collections : Dictionary = {} - ## A Dictionary containing all authentication fields for the current logged user. ## @type Dictionary var auth : Dictionary @@ -81,25 +69,11 @@ var _request_list_node : HTTPRequest var _requests_queue : Array = [] var _current_query : FirestoreQuery -var _http_request_pool := [] - var _offline: bool = false : set = _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_at(i) - continue # Just to skip set_meta on a queue_freed request - request.set_meta("lifetime", lifetime) - - ## Returns a reference collection by its [i]path[/i]. ## ## The returned object will be of [code]FirestoreCollection[/code] type. @@ -107,18 +81,19 @@ func _process(delta : float) -> void: ## @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 - collections[path] = coll - add_child(coll) - return coll - else: - return collections[path] + 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. @@ -140,19 +115,17 @@ func collection(path : String) -> FirestoreCollection: ## @args query ## @arg-types FirestoreQuery ## @return FirestoreTask -func query(query : FirestoreQuery) -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.result_query.connect(_on_result_query) # In theory, this and the following could be a CONNECT_ONE_SHOT, but I'm iffy on whether or not that might break any client code, so leaving this for now. - firestore_task.task_error.connect(_on_task_error) - firestore_task.action = FirestoreTask.Task.TASK_QUERY +func query(query : FirestoreQuery) -> Array: + 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_suffix - firestore_task.data = query - firestore_task._fields = JSON.stringify(body) - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task + task.data = query + task._fields = JSON.stringify(body) + task._url = url + _pooled_request(task) + return await _handle_task_finished(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 checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. @@ -169,11 +142,9 @@ func query(query : FirestoreQuery) -> FirestoreTask: ## @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.listed_documents.connect(_on_listed_documents) # Same as above with one shot connections - firestore_task.task_error.connect(_on_task_error) - firestore_task.action = FirestoreTask.Task.TASK_LIST +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) @@ -182,10 +153,11 @@ func list(path : String = "", page_size : int = 0, page_token : String = "", ord 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 + task.data = [path, page_size, page_token, order_by] + task._url = url + _pooled_request(task) + + return await _handle_task_finished(task) func set_networking(value: bool) -> void: @@ -200,8 +172,9 @@ func enable_networking() -> void: return networking = true _base_url = _base_url.replace("storeoffline", "firestore") - for key in collections: - collections[key]._base_url = _base_url + for coll in get_children(): + if coll is FirestoreCollection: + coll._base_url = _base_url func disable_networking() -> void: @@ -210,8 +183,9 @@ func disable_networking() -> void: 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 + for coll in get_children(): + if coll is FirestoreCollection: + coll._base_url = _base_url func _set_offline(value: bool) -> void: @@ -254,55 +228,29 @@ func _pooled_request(task : FirestoreTask) -> void: if not Firebase.emulating: task._headers = PackedStringArray([_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 - Utilities.fix_http_request(http_request) - _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) + 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_listed_documents(_listed_documents : Array): - listed_documents.emit(_listed_documents) - -func _on_result_query(result : Array): - result_query.emit(result) - -func _on_task_error(code : int, status : String, message : String, task : int): - task_error.emit(code, status, message) - Firebase._printerr(message) - func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: auth = auth_result - for key in collections: - collections[key].auth = auth - + 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 : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: - request.get_meta("task")._on_request_completed(result, response_code, headers, body) - request.set_meta("requesting", false) - + for coll in get_children(): + if coll is FirestoreCollection: + coll.auth = auth func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: _set_offline(result != HTTPRequest.RESULT_SUCCESS) @@ -318,3 +266,11 @@ func _check_auth_error(code : int, message : String) -> void: 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_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 7be0db6..94877bf 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -7,12 +7,7 @@ class_name FirestoreCollection extends Node -signal add_document(doc) -signal get_document(doc) -signal update_document(doc) -signal commit_document(result) -signal delete_document(deleted) -signal error(code,status,message) +signal error(error_result) const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " @@ -28,44 +23,73 @@ 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_doc(document_id : String) -> FirestoreTask: +func get_doc(document_id : String, from_cache : bool = false, update_cache : 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") - task.get_document.connect(_on_get_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) _process_request(task, document_id, url) - return task + var result = await Firebase.Firestore._handle_task_finished(task) + if result != null: + result.collection_name = collection_name + if result != null: + var changes = {"added":[], "changed":[], "removed":[]} + for child in get_children(): + if child.doc_name == document_id: + var child_listener = result.get_child(0) if result.get_child_count() == 1 else null + if child_listener != null: + changes = child._changes.duplicate() + child.remove_child(child_listener) # Transfer the listener over so it's not freed all the way through + result.add_child(child_listener) + child.queue_free() + + result._changes = changes + add_child(result) + 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, data : Dictionary = {}) -> FirestoreTask: +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 - task.add_document.connect(_on_add_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) _process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data))) - return task - + 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 should already exist + break + + add_child(result) + result.collection_name = collection_name + return result + ## @args document_id, fields ## @arg-defaults , {} ## @return FirestoreTask # used to UPDATE a document, specify @documentID, @fields -func update(document : FirestoreDocument) -> FirestoreTask: +func update(document : FirestoreDocument, update_cache := false) -> FirestoreDocument: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_PATCH task.data = collection_name + "/" + document.doc_name @@ -76,24 +100,19 @@ func update(document : FirestoreDocument) -> FirestoreTask: url = url.rstrip("&") for key in document.keys(): - if document.get_value(key) == null: + if document.get_value(key) == null and not document.is_null_value(key): document._erase(key) - task.update_document.connect(_on_update_document) - task.task_finished.connect(_on_task_finished.bind(document.doc_name), CONNECT_DEFERRED) - - var body = JSON.stringify({"fields": document.document}, " ") + var body = JSON.stringify({"fields": document.document}) _process_request(task, document.doc_name, url, body) - return task + return await Firebase.Firestore._handle_task_finished(task) -func commit(document : FirestoreDocument) -> FirestoreTask: +func commit(document : FirestoreDocument) -> Dictionary: var task : FirestoreTask = FirestoreTask.new() task.action = FirestoreTask.Task.TASK_COMMIT var url = get_database_url("commit") - task.commit_document.connect(_on_commit_document) - task.task_finished.connect(_on_task_finished.bind(document.doc_name), CONNECT_DEFERRED) - + document._transforms.set_config( { "extended_url": _extended_url, @@ -103,21 +122,26 @@ func commit(document : FirestoreDocument) -> FirestoreTask: var body = document._transforms.serialize() _process_request(task, document.doc_name, url, JSON.stringify(body)) - return task + return await Firebase.Firestore._handle_task_finished(task) ## @args document_id ## @return FirestoreTask # used to DELETE a document, specify @document_id -func delete(document_id : String) -> FirestoreTask: +func delete(document : FirestoreDocument) -> bool: + var doc_name = document.doc_name 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.delete_document.connect(_on_delete_document) - task.task_finished.connect(_on_task_finished.bind(document_id), CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task + 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) + #if result: + #for node in get_children(): + #if node.doc_name == doc_name: + #node.free() + #break + + return result # ----------------- Functions func _get_request_url() -> String: @@ -125,9 +149,6 @@ func _get_request_url() -> String: func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: - if not task.task_error.is_connected(_on_error): - task.task_error.connect(_on_error) - if auth == null or auth.is_empty(): Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() @@ -140,41 +161,21 @@ func _process_request(task : FirestoreTask, document_id : String, url : String, task._url = url task._fields = fields task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - if _request_queues.has(document_id) and not _request_queues[document_id].is_empty(): - _request_queues[document_id].append(task) - else: - _request_queues[document_id] = [] - Firebase.Firestore._pooled_request(task) - -func _on_task_finished(task : FirestoreTask, document_id : String) -> void: - if not _request_queues[document_id].is_empty(): - task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) - -# -------------------- Higher level of communication with signals -func _on_get_document(document : FirestoreDocument): - get_document.emit(document) - -func _on_add_document(document : FirestoreDocument): - document.collection_name = collection_name - add_document.emit(document) - -func _on_update_document(document : FirestoreDocument): - update_document.emit(document) - -func _on_delete_document(deleted): - delete_document.emit(deleted) - -func _on_error(code, status, message, task): - error.emit(code, status, message) - Firebase._printerr(message) - -func _on_commit_document(result): - commit_document.emit(result) - -func _add_document_listener(document_path : String, with_func : Callable): - var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate() - add_child(listener) - return listener.connect_to_firestore(_config) + Firebase.Firestore._pooled_request(task) + +func _add_document_listener(document : FirestoreDocument, poll_time : float = 200.0): + var doc = document + if doc == null: + assert(false, "Document must be gotten at least once before listening to changes for it with a listener") + return + + if doc.get_child_count() >= 1: # Only one listener per + assert(false, "Multiple listeners not allowed for the same document yet") + return + + var listener = FirestoreListener.new(doc, poll_time) + doc.add_child(listener) + return listener.enable_connection() func get_database_url(append) -> String: return _base_url + _extended_url.rstrip("/") + ":" + append diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index 4c9a4fb..70d5bc7 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -4,7 +4,7 @@ ## Documentation TODO. @tool class_name FirestoreDocument -extends RefCounted +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 @@ -17,6 +17,8 @@ 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 = {}): _transforms = FieldTransformArray.new() @@ -27,40 +29,45 @@ func _init(doc : Dictionary = {}): self.create_time = doc.createTime +func replace(with : Dictionary) -> void: + var current = document.duplicate() + document = with + + var changes = { + "added": [], "removed": [], "updated": [] + } + + for key in current.keys(): + if not document.has(key): + changes.removed.push_back({key : ""}) + else: + var new_value = Utilities.from_firebase_type(document[key]) + var old_value = Utilities.from_firebase_type(current[key]) + changes.updated.push_back({ key : { "old": old_value, "new" : new_value }}) + +func is_null_value(key) -> bool: + return document.has(key) and document[key].keys()[0] == "nullValue" + func add_field_transform(transform : FieldTransform) -> void: _transforms.push_back(transform) func remove_field(field_path : String) -> void: if document.has(field_path): document[field_path] = null - + func _erase(field_path : String) -> void: document.erase(field_path) func add_or_update_field(field_path : String, value : Variant) -> void: - document[field_path] = Utilities.to_firebase_type(value) + var converted_value = Utilities.to_firebase_type(value) + document[field_path] = converted_value -func on_snapshot(listener : Callable): +func on_snapshot(listener : Callable, poll_time : float) -> FirestoreListener.FirestoreListenerConnection: var collection = Firebase.Firestore.collection(collection_name) - var result = collection._add_document_listener(doc_name, listener) + var result = collection._add_document_listener(self, poll_time) + result.connection.document_updated.connect(listener, CONNECT_REFERENCE_COUNTED) # Ensure nothing leaks when connecting more than once, nor errors show up if already connected return result -func get_listener_request(config, _extended_url, _collection_name) -> Dictionary: - var _separator = "/" - return \ - { - "addTarget": { - "documents": [_extended_url + _collection_name + _separator + doc_name] - } - } - -# Call print(document) to return directly this document formatted -func _to_string() -> String: - 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}) - func get_value(property : StringName) -> Variant: if property == "doc_name": return doc_name @@ -82,3 +89,10 @@ func _set(property: StringName, value: Variant) -> bool: func keys(): return document.keys() + +# Call print(document) to return directly this document formatted +func _to_string() -> String: + 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_listener.gd b/addons/godot-firebase/firestore/firestore_listener.gd index 3d0eef8..c054154 100644 --- a/addons/godot-firebase/firestore/firestore_listener.gd +++ b/addons/godot-firebase/firestore/firestore_listener.gd @@ -1,135 +1,172 @@ class_name FirestoreListener extends Node +# +##var http_client = HTTPClient.new() +# +#var tcp_stream = StreamPeerTCP.new() +#var tls_stream = StreamPeerTLS.new() +# +#var project_id = "roundtable-5c241" +#var collection_path = "Firebasetester" +#var document_id = "Document1" +#var connected = false +#var api_key +# +#var response_body = PackedByteArray() +# +#var host +# +#var request_to_send : Callable +#var request_sent := false +# +#func _ready(): + #set_process(false) +# +#func connect_to_firestore(config): + # + # + #project_id = config.projectId.uri_encode() + #api_key = config.apiKey + #host = "firestore.googleapis.com" + #var port = 443 # Use port 443 for HTTPS + #var tlsoptions = TLSOptions.client_unsafe() + # + #var err = tcp_stream.connect_to_host(host, port) + ##var err = http_client.connect_to_host(host, port, tlsoptions) + #if err != OK: + #print("Failed to connect to host: ", err) + #else: + #print("Connected to host") + # + #err = tls_stream.connect_to_stream(tcp_stream, "", tlsoptions) + #if err != OK: + #print("Something went wrong with the TLS handshake") + #print("Error: ", err) + #return + # + #if tls_stream.get_status() != StreamPeerTLS.STATUS_CONNECTED: + #print("TLS handshake failed") + #return + # + #var path = "/google.firestore.v1.Firestore/Listen/channel" + "?VER=8&database=projects%2Froundtable-5c241%2Fdatabases%2F(default)&RID=97627&CVER=22&X-HTTP-Session-Id=gsessionid&zx=cjl2twashomb&t=1" + #var body = { + #"addTarget": + #{ + #"documents": + #{ + #"documents": ["projects/%s/databases/(default)/documents/%s/%s" % [project_id, collection_path, document_id]] + #} + #} + #} + # + #var encoded_body = encode_body(body) + #encoded_body = "&req0___data__=" + encoded_body + # + #var headers = [ + #"Authorization: Bearer %s" % Firebase.Auth.auth.idtoken, + #"Content-Type: x-www-form-urlencoded", + #"Content-Length: " + str(encoded_body.length()), + #":authority: firestore.googleapis.com", + #":method: POST", + #":path: /google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Froundtable-5c241%2Fdatabases%2F(default)&RID=97627&CVER=22&X-HTTP-Session-Id=gsessionid&zx=cjl2twashomb&t=1", + #":scheme: https", + #] + # + #var request = "POST %s HTTP/2.0\r\n" % path + #request += "\r\n".join(headers) + #request += encoded_body + # + #print("Final path: ", path) + #set_process(true) + #send_request(request) + # + #return self +# +#func _process(delta: float) -> void: + #if not request_sent: + #return + # + #if tls_stream.get_available_bytes() > 0: + #var chunk = tls_stream.read(tls_stream.get_available_bytes()) + #response_body += chunk.get_utf8_string() + #print("Response body: ", response_body) + # +#func send_request(request : String): + #var err = tls_stream.put_data(request.to_utf8_buffer()) + #if err != OK: + #print("Error sending: ", request) + #else: + #request_sent = true +# +#func encode_body(body : Dictionary) -> String: + #var query_string = [] + #for key in body.keys(): + #var value = body[key] + #if typeof(value) == TYPE_DICTIONARY: + #value = JSON.stringify(value) + # + #value = value.replace("\"", "") + #key = key.replace("\"", "") + #query_string.append(key.uri_encode() + "=".uri_encode() + value.uri_encode()) + #return "&".join(query_string) +# +#func handle_response(chunk): + #var response_text = chunk.get_string_from_utf8() + #print("Received chunk: ", response_text) + #var response_json = Utilities.get_json_data(response_text) + #if response_json.error == OK: + #print("Parsed JSON: ", response_json.result) + #else: + #print("Failed to parse JSON: ", response_json.error_string()) -var http_client = HTTPClient.new() +signal document_updated(snapshot : Dictionary) -var project_id = "roundtable-5c241" -var collection_path = "Firebasetester" -var document_id = "Document1" -var connected = false -var api_key +const MinPollTime = 60 # seconds -var response_body = PackedByteArray() +var _doc_name : String +var _tracked_changes : Dictionary +var _poll_time : float +var _collection : FirestoreCollection -var host +var _timer = Timer.new() +var _total_time = 0.0 -var request_to_send : Callable -var request_sent := false +var _is_requesting_update := false -func _ready(): - set_process(false) - -func connect_to_firestore(config): - project_id = config.projectId.uri_encode() - api_key = config.apiKey - host = "firestore.googleapis.com" - var port = 443 # Use port 443 for HTTPS - var tlsoptions = TLSOptions.client(null, "") +func _init(document : FirestoreDocument, poll_time : float) -> void: + _poll_time = max(poll_time, MinPollTime) + _tracked_changes = document._changes.duplicate() + _doc_name = document.doc_name + _collection = Firebase.Firestore.collection(document.collection_name) - var err = http_client.connect_to_host(host, port, tlsoptions) - if err != OK: - print("Failed to connect to host: ", err) - else: - print("Connected to host") - set_process(true) # Start polling - - var path = "/google.firestore.v1.Firestore/Listen/channel" + "?VER=8&database=projects%2Froundtable-5c241%2Fdatabases%2F(default)&RID=97627&CVER=22&X-HTTP-Session-Id=gsessionid&zx=cjl2twashomb&t=1" - var body = { - "addTarget": - { - "documents": - { - "documents": ["projects/%s/databases/(default)/documents/%s/%s" % [project_id, collection_path, document_id]] - } - } - } - - var encoded_body = encode_body(body) - encoded_body = "&req0___data__=" + encoded_body - - var headers = [ - "Authorization: Bearer %s" % Firebase.Auth.auth.idtoken, - "Content-Type: x-www-form-urlencoded", - "Content-Length: " + str(encoded_body.length()), - ":authority: firestore.googleapis.com", - ":method: POST", - ":path: /google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Froundtable-5c241%2Fdatabases%2F(default)&RID=97627&CVER=22&X-HTTP-Session-Id=gsessionid&zx=cjl2twashomb&t=1", - ":scheme: https", - ] - - print("Final path: ", path) - set_process(true) - request_to_send = func(): send_request(path, headers, encoded_body) - - return self - -func _process(delta: float) -> void: - var status = http_client.get_status() - if status in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING, HTTPClient.STATUS_REQUESTING]: - http_client.poll() - return + add_child(_timer) - if http_client.get_status() == HTTPClient.STATUS_BODY: - http_client.poll() - response_body += http_client.read_response_body_chunk() - OS.delay_msec(50) +func enable_connection() -> FirestoreListenerConnection: + _timer.wait_time = 1 + _timer.timeout.connect(_update_document, CONNECT_REFERENCE_COUNTED) + _timer.start() + return FirestoreListenerConnection.new(self) - if http_client.has_response(): - var response_code = http_client.get_response_code() - var response_headers = http_client.get_response_headers_as_dictionary() - var response_body = PackedByteArray() +func _update_document(): + _total_time += _timer.wait_time + var stored_doc = await _collection.get_doc(_doc_name, _total_time <= _poll_time) + if stored_doc != null: + var result = stored_doc._changes - if response_code == 200: - print("Response code: ", response_code) - print("Response headers: ", response_headers) - if response_body.size() == 0: - print("Request returned size zero") - else: - var response_json = Utilities.get_json_data(response_body) - if response_json.error == OK: - var response_data = response_json.result - print("Request successful: %s" % response_data) - else: - print("JSON parse error: %s" % response_json.error_string) - else: - print("Request failed with status code: %s" % response_code) - print("Response body: %s" % Utilities.get_json_data(response_body)) + if _tracked_changes != result: + document_updated.emit(_tracked_changes) + _tracked_changes = result.duplicate() + else: + var result = { + "deleted": { _doc_name: "" } # Not sure if this works, find out what happens when you delete a document and attempt to get it + } - return - - if status != HTTPClient.STATUS_CONNECTED: - print("Failed to connect") - set_process(false) - return + document_updated.emit(result) + _timer.stop() - if not request_sent: - request_to_send.call() - request_sent = true + if _total_time > _poll_time: + _total_time = 0.0 -func send_request(url, headers, body): - http_client.request(HTTPClient.METHOD_POST, url, headers, body) - -func encode_body(body : Dictionary) -> String: - var query_string = [] - for key in body.keys(): - var value = body[key] - if typeof(value) == TYPE_DICTIONARY: - value = JSON.stringify(value) - - value = value.replace("\"", "") - key = key.replace("\"", "") - query_string.append(key.uri_encode() + "=".uri_encode() + value.uri_encode()) - return "&".join(query_string) - -func handle_response(chunk): - var response_text = chunk.get_string_from_utf8() - print("Received chunk: ", response_text) - var response_json = Utilities.get_json_data(response_text) - if response_json.error == OK: - print("Parsed JSON: ", response_json.result) - else: - print("Failed to parse JSON: ", response_json.error_string()) - class FirestoreListenerConnection extends RefCounted: var connection @@ -137,5 +174,5 @@ class FirestoreListenerConnection extends RefCounted: connection = connection_node func stop(): - connection.tcp_client.disconnect_from_host() - connection.free() + if connection != null and is_instance_valid(connection): + connection.free() diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 8fd659f..9fad023 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -23,31 +23,7 @@ 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 checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result.. -## @arg-types FirestoreDocument -signal add_document(doc) -## Emitted when a [code]get(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. -## @arg-types FirestoreDocument -signal get_document(doc) -## Emitted when a [code]update(document)[/code] request checked a [class FirebaseCollection] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]null[/code] will be passed as a result. -## @arg-types FirestoreDocument -signal update_document(doc) -## Emitted when a [code]write(document)[/code] request for a document is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code]result[/code] will be passed as a result. -## @arg-types FirestoreDocument -signal commit_document(result) -## Emitted when a [code]delete(document)[/code] request checked a [class FirebaseCollection] is successfully completed and [code]true[/code] will be passed. [code]error()[/code] signal will be emitted otherwise and [code]false[/code] will be passed as a result. -## @arg-types bool -signal delete_document(success) -## Emitted when a [code]list(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code][][/code] will be passed as a result.. -## @arg-types Array -signal listed_documents(documents) -## Emitted when a [code]query(collection_id)[/code] request checked [class FirebaseFirestore] is successfully completed. [code]error()[/code] signal will be emitted otherwise and [code][][/code] will be passed as a result. -## @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 @@ -79,8 +55,6 @@ var action : int = -1 : set = set_action var data var error : Dictionary var document : FirestoreDocument -## Whether the data came from cache. -var from_cache : bool = false var _response_headers : PackedStringArray = PackedStringArray() var _response_code : int = 0 @@ -95,31 +69,21 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt if bod != "": bod = Utilities.get_json_data(bod) - 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 # Probably going to regret this... if response_code == HTTPClient.RESPONSE_OK: - data = bod match action: - Task.TASK_POST: + Task.TASK_POST, Task.TASK_GET, Task.TASK_PATCH: document = FirestoreDocument.new(bod) - add_document.emit(document) - Task.TASK_GET: - document = FirestoreDocument.new(bod) - get_document.emit(document) - Task.TASK_PATCH: - document = FirestoreDocument.new(bod) - update_document.emit(document) + data = document Task.TASK_DELETE: - delete_document.emit(true) + data = true Task.TASK_QUERY: data = [] for doc in bod: if doc.has('document'): data.append(FirestoreDocument.new(doc.document)) - result_query.emit(data) Task.TASK_LIST: data = [] if bod.has('documents'): @@ -127,48 +91,36 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt data.append(FirestoreDocument.new(doc)) if bod.has("nextPageToken"): data.append(bod.nextPageToken) - listed_documents.emit(data) Task.TASK_COMMIT: - commit_document.emit(bod) + 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) - emit_error(task_error, bod, action) - match action: - Task.TASK_POST: - add_document.emit(null) - Task.TASK_GET: - get_document.emit(null) - Task.TASK_PATCH: - update_document.emit(null) - Task.TASK_DELETE: - delete_document.emit(false) - Task.TASK_QUERY: - data = [] - result_query.emit(data) - Task.TASK_LIST: - data = [] - listed_documents.emit(data) - Task.TASK_COMMIT: - commit_document.emit(null) - - task_finished.emit(self) - -func emit_error(_signal, 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 - - _signal.emit(error.code, error.status, error.message, task) - - return - - _signal.emit(1, 0, "Unknown error", task) + 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 @@ -185,9 +137,6 @@ func set_action(value : int) -> void: _method = HTTPClient.METHOD_POST -func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: - return body # Removing caching for now, hopefully this works without killing everyone and everything - func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: var ret := dic_a.duplicate(true) for key in dic_b: diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index 605a180..8ccfa97 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -51,7 +51,7 @@ var _http_request_pool : Array = [] 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): @@ -68,6 +68,7 @@ func _process(delta : float) -> void: ## @args ## @return FunctionTask func execute(function: String, method: int, params: Dictionary = {}, body: Dictionary = {}) -> FunctionTask: + 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) diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index ff4421b..d50dc54 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -44,14 +44,17 @@ func _test_error(data) -> void: _test_finished() func _cleanup_previous_run(): - var del_task : FirestoreTask = _collection.delete("Document1") - var deleted = await del_task.delete_document - + 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.button_pressed = true - _test_firestore() + await _test_firestore() # Function called when login to Firebase has failed # Ends the test and prints the error to the GUI console @@ -75,28 +78,28 @@ func _test_firestore() -> void: _print_to_console("\nConnecting to collection 'Firebasetester'") _collection = Firebase.Firestore.collection('Firebasetester') - # Connect to signals needed for testing - _collection.add_document.connect(on_document_add) - _collection.get_document.connect(on_document_get) - _collection.update_document.connect(on_document_update) - _collection.delete_document.connect(on_document_delete) - _collection.error.connect(on_document_error) - #await _cleanup_previous_run() + 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', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) - #_document = await add_task.add_document - #$add_document.button_pressed = true + _print_to_console("Trying to add a document") + _document = await _collection.add("Document1", {'name': 'Document1', 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) + $add_document.button_pressed = true # ## Get Document1 (Document that has been added from the previous step) - #_print_to_console("Trying to get Document1") - var get_task = _collection.get_doc('Document1') - _document = await _collection.get_document - #if(_document == null): - #_test_error("Failed to get document") - #return - #else: - #$get_document.button_pressed = true + _print_to_console("Trying to get Document1") + _document = await _collection.get_doc('Document1') + + if(_document == null): + _test_error("Failed to get document") + return + + _document = await _collection.get_doc('Document1', true) + + if(_document == null): + _test_error("Failed to get document from cache") + return + + + # ## Print Document1 to the console GUI #_print_to_console("Trying to print contents of Document1") @@ -177,72 +180,38 @@ func _test_firestore() -> void: func run_listener_tests() -> void: #var add_task : FirestoreTask = _collection.add("Document1", {'name': 'Document1', 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) #_document = await add_task.add_document - listener_test_count = 0 - var listener = _document.on_snapshot(func(result): print(result)) - await get_tree().create_timer(5.0).timeout + _document = await _collection.get_doc("Document1", false) + + var listener = _document.on_snapshot( + func(result): + print(result) + , .5) # Attempt to update every half second const new_doc_name = 'NewDocument' _document.add_or_update_field("name", new_doc_name) + print("Name updated") + _document = await _collection.update(_document, true) + print("Document updated") - var update_task : FirestoreTask = _collection.update(_document) - _document = await update_task.update_document - # - #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 up_task = _collection.commit(_document) - #var result = await up_task.commit_document - # - # - await get_tree().create_timer(10.0).timeout - var get_task = _collection.get_doc('Document1') - _document = await _collection.get_document - - - _print_to_console("Count outside: " + str(listener_test_count)) - - #listener.stop() - - + await get_tree().create_timer(1.0).timeout _document.add_or_update_field("name", new_doc_name + "2") + print("Updated a second time") - update_task = _collection.update(_document) - _document = await update_task.update_document + await get_tree().create_timer(1.0).timeout + var deleted = await _collection.delete(_document) + if deleted: + print("Deleted document") + await get_tree().create_timer(5.0).timeout - _print_to_console("Count: " + str(listener_test_count)) + _document = await _collection.add("Document1", {'name': 'Document1', 'active': 'true', "serviker_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) + print("Document re-added") + print("Listener stopped") + listener.stop() - -# 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("Current document after update: " + str(document_updated)) - _print_to_console("Document Updated successfully") - -# Function called when a document has been deleted in Firestore successfully -func on_document_delete(deleted) -> void: - _print_to_console("Document deleted: " + str(deleted)) - -# 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") + _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): From b5864e7fe18e221a501ddd03d29f3e070fb70018 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Tue, 4 Jun 2024 08:11:53 -0400 Subject: [PATCH 14/22] Big refactor part 3 --- addons/godot-firebase/Utilities.gd | 12 +- .../firestore/firestore_collection.gd | 97 +++++----- .../firestore/firestore_document.gd | 72 ++++++-- .../firestore/firestore_listener.gd | 173 +++--------------- fonts/PermanentMarker.ttf.import | 1 + tests/firestore/firestore.gd | 155 ++++++++-------- 6 files changed, 210 insertions(+), 300 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index fdf1095..ed26a71 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -57,7 +57,7 @@ static func from_firebase_type(value : Variant) -> Variant: return null if value.has("mapValue"): - value = _from_firebase_type_recursive(value.value()[0].fields) + value = _from_firebase_type_recursive(value.values()[0].fields) elif value.has("timestampValue"): value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) else: @@ -72,7 +72,7 @@ static func _from_firebase_type_recursive(value : Variant) -> Variant: if value.has("mapValue") or value.has("timestampValue"): value = _from_firebase_type_recursive(value.value()[0].fields) else: - value = value.value()[0] + value = value.values()[0] return value @@ -316,3 +316,11 @@ class ObservableDictionary extends RefCounted: 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/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 94877bf..0dae95f 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -29,7 +29,7 @@ var _documents := {} ## @args document_id ## @return FirestoreTask ## used to GET a document from the collection, specify @document_id -func get_doc(document_id : String, from_cache : bool = false, update_cache : bool = false) -> FirestoreDocument: +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(): @@ -44,20 +44,11 @@ func get_doc(document_id : String, from_cache : bool = false, update_cache : boo _process_request(task, document_id, url) var result = await Firebase.Firestore._handle_task_finished(task) if result != null: - result.collection_name = collection_name - if result != null: - var changes = {"added":[], "changed":[], "removed":[]} - for child in get_children(): - if child.doc_name == document_id: - var child_listener = result.get_child(0) if result.get_child_count() == 1 else null - if child_listener != null: - changes = child._changes.duplicate() - child.remove_child(child_listener) # Transfer the listener over so it's not freed all the way through - result.add_child(child_listener) - child.queue_free() - - result._changes = changes - add_child(result) + for child in get_children(): + if child.doc_name == document_id: + child.replace(result, true) + result = child + break else: print("get_document returned null for %s %s" % [collection_name, document_id]) @@ -65,8 +56,8 @@ func get_doc(document_id : String, from_cache : bool = false, update_cache : boo ## @args document_id, fields ## @arg-defaults , {} -## @return FirestoreTask -## used to SAVE/ADD a new document to the collection, specify @documentID and @fields +## @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 @@ -78,18 +69,17 @@ func add(document_id : String, data : Dictionary = {}) -> FirestoreDocument: if result != null: for child in get_children(): if child.doc_name == document_id: - child.free() # Consider throwing an error for this since it should already exist + child.free() # Consider throwing an error for this since it shouldn't already exist break - add_child(result) result.collection_name = collection_name + add_child(result, true) return result -## @args document_id, fields -## @arg-defaults , {} -## @return FirestoreTask -# used to UPDATE a document, specify @documentID, @fields -func update(document : FirestoreDocument, update_cache := false) -> FirestoreDocument: +## @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 @@ -100,14 +90,33 @@ func update(document : FirestoreDocument, update_cache := false) -> FirestoreDoc url = url.rstrip("&") for key in document.keys(): - if document.get_value(key) == null and not document.is_null_value(key): + if document.get_value(key) == null: document._erase(key) - var body = JSON.stringify({"fields": document.document}) + 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) - return await Firebase.Firestore._handle_task_finished(task) + 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 + 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 @@ -121,12 +130,14 @@ func commit(document : FirestoreDocument) -> Dictionary: ) # Only place we can set this is here, oofness var body = document._transforms.serialize() + document.clear_field_transforms() _process_request(task, document.doc_name, url, JSON.stringify(body)) - return await Firebase.Firestore._handle_task_finished(task) + + return await Firebase.Firestore._handle_task_finished(task) # Not implementing the follow-up get here as user may have a listener that's already listening for changes, but user should call get if they don't ## @args document_id ## @return FirestoreTask -# used to DELETE a document, specify @document_id +# used to DELETE a document, specify the document func delete(document : FirestoreDocument) -> bool: var doc_name = document.doc_name var task : FirestoreTask = FirestoreTask.new() @@ -135,19 +146,19 @@ func delete(document : FirestoreDocument) -> bool: 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) - #if result: - #for node in get_children(): - #if node.doc_name == doc_name: - #node.free() - #break + + # 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 -# ----------------- Functions func _get_request_url() -> String: return _base_url + _extended_url + collection_name - func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: if auth == null or auth.is_empty(): Firebase._print("Unauthenticated request issued...") @@ -163,19 +174,5 @@ func _process_request(task : FirestoreTask, document_id : String, url : String, task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) Firebase.Firestore._pooled_request(task) -func _add_document_listener(document : FirestoreDocument, poll_time : float = 200.0): - var doc = document - if doc == null: - assert(false, "Document must be gotten at least once before listening to changes for it with a listener") - return - - if doc.get_child_count() >= 1: # Only one listener per - assert(false, "Multiple listeners not allowed for the same document yet") - return - - var listener = FirestoreListener.new(doc, poll_time) - doc.add_child(listener) - return listener.enable_connection() - func get_database_url(append) -> String: return _base_url + _extended_url.rstrip("/") + ":" + append diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index 70d5bc7..f0f9249 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -16,7 +16,6 @@ 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 = {}): @@ -29,43 +28,90 @@ func _init(doc : Dictionary = {}): self.create_time = doc.createTime -func replace(with : Dictionary) -> void: +func replace(with : FirestoreDocument, is_listener := false) -> void: var current = document.duplicate() - document = with + document = with.document var changes = { - "added": [], "removed": [], "updated": [] + "added": [], "removed": [], "updated": [], "is_listener": is_listener } for key in current.keys(): if not document.has(key): - changes.removed.push_back({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]) - changes.updated.push_back({ key : { "old": old_value, "new" : new_value }}) + if new_value != old_value: + if old_value == null: + changes.removed.push_back({ "key" : key }) # ?? + else: + 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()): + changed.emit(changes) func is_null_value(key) -> bool: - return document.has(key) and document[key].keys()[0] == "nullValue" + return document.has(key) and Utilities.from_firebase_type(document[key]) == null +# 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: _transforms.push_back(transform) +func remove_field_transform(transform : FieldTransform) -> void: + _transforms.erase(transform) + +func clear_field_transforms() -> void: + _transforms.transforms.clear() + func remove_field(field_path : String) -> void: if document.has(field_path): - document[field_path] = null + document[field_path] = Utilities.to_firebase_type(null) + + var changes = { + "added": [], "removed": [], "updated": [], "is_listener": false + } + + changes.removed.push_back({ "key" : field_path }) + changed.emit(changes) func _erase(field_path : String) -> void: document.erase(field_path) -func add_or_update_field(field_path : String, value : Variant) -> void: +func add_or_update_field(field_path : String, value : Variant) -> void: + var changes = { + "added": [], "removed": [], "updated": [], "is_listener": false + } + + 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 }) -func on_snapshot(listener : Callable, poll_time : float) -> FirestoreListener.FirestoreListenerConnection: - var collection = Firebase.Firestore.collection(collection_name) - var result = collection._add_document_listener(self, poll_time) - result.connection.document_updated.connect(listener, CONNECT_REFERENCE_COUNTED) # Ensure nothing leaks when connecting more than once, nor errors show up if already connected + changed.emit(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 + + changed.connect(when_called, CONNECT_REFERENCE_COUNTED) + var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate() + add_child(listener) + listener.initialize_listener(collection_name, doc_name, poll_time) + listener.owner = self + var result = listener.enable_connection() return result func get_value(property : StringName) -> Variant: diff --git a/addons/godot-firebase/firestore/firestore_listener.gd b/addons/godot-firebase/firestore/firestore_listener.gd index c054154..7808a5c 100644 --- a/addons/godot-firebase/firestore/firestore_listener.gd +++ b/addons/godot-firebase/firestore/firestore_listener.gd @@ -1,171 +1,39 @@ class_name FirestoreListener extends Node -# -##var http_client = HTTPClient.new() -# -#var tcp_stream = StreamPeerTCP.new() -#var tls_stream = StreamPeerTLS.new() -# -#var project_id = "roundtable-5c241" -#var collection_path = "Firebasetester" -#var document_id = "Document1" -#var connected = false -#var api_key -# -#var response_body = PackedByteArray() -# -#var host -# -#var request_to_send : Callable -#var request_sent := false -# -#func _ready(): - #set_process(false) -# -#func connect_to_firestore(config): - # - # - #project_id = config.projectId.uri_encode() - #api_key = config.apiKey - #host = "firestore.googleapis.com" - #var port = 443 # Use port 443 for HTTPS - #var tlsoptions = TLSOptions.client_unsafe() - # - #var err = tcp_stream.connect_to_host(host, port) - ##var err = http_client.connect_to_host(host, port, tlsoptions) - #if err != OK: - #print("Failed to connect to host: ", err) - #else: - #print("Connected to host") - # - #err = tls_stream.connect_to_stream(tcp_stream, "", tlsoptions) - #if err != OK: - #print("Something went wrong with the TLS handshake") - #print("Error: ", err) - #return - # - #if tls_stream.get_status() != StreamPeerTLS.STATUS_CONNECTED: - #print("TLS handshake failed") - #return - # - #var path = "/google.firestore.v1.Firestore/Listen/channel" + "?VER=8&database=projects%2Froundtable-5c241%2Fdatabases%2F(default)&RID=97627&CVER=22&X-HTTP-Session-Id=gsessionid&zx=cjl2twashomb&t=1" - #var body = { - #"addTarget": - #{ - #"documents": - #{ - #"documents": ["projects/%s/databases/(default)/documents/%s/%s" % [project_id, collection_path, document_id]] - #} - #} - #} - # - #var encoded_body = encode_body(body) - #encoded_body = "&req0___data__=" + encoded_body - # - #var headers = [ - #"Authorization: Bearer %s" % Firebase.Auth.auth.idtoken, - #"Content-Type: x-www-form-urlencoded", - #"Content-Length: " + str(encoded_body.length()), - #":authority: firestore.googleapis.com", - #":method: POST", - #":path: /google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Froundtable-5c241%2Fdatabases%2F(default)&RID=97627&CVER=22&X-HTTP-Session-Id=gsessionid&zx=cjl2twashomb&t=1", - #":scheme: https", - #] - # - #var request = "POST %s HTTP/2.0\r\n" % path - #request += "\r\n".join(headers) - #request += encoded_body - # - #print("Final path: ", path) - #set_process(true) - #send_request(request) - # - #return self -# -#func _process(delta: float) -> void: - #if not request_sent: - #return - # - #if tls_stream.get_available_bytes() > 0: - #var chunk = tls_stream.read(tls_stream.get_available_bytes()) - #response_body += chunk.get_utf8_string() - #print("Response body: ", response_body) - # -#func send_request(request : String): - #var err = tls_stream.put_data(request.to_utf8_buffer()) - #if err != OK: - #print("Error sending: ", request) - #else: - #request_sent = true -# -#func encode_body(body : Dictionary) -> String: - #var query_string = [] - #for key in body.keys(): - #var value = body[key] - #if typeof(value) == TYPE_DICTIONARY: - #value = JSON.stringify(value) - # - #value = value.replace("\"", "") - #key = key.replace("\"", "") - #query_string.append(key.uri_encode() + "=".uri_encode() + value.uri_encode()) - #return "&".join(query_string) -# -#func handle_response(chunk): - #var response_text = chunk.get_string_from_utf8() - #print("Received chunk: ", response_text) - #var response_json = Utilities.get_json_data(response_text) - #if response_json.error == OK: - #print("Parsed JSON: ", response_json.result) - #else: - #print("Failed to parse JSON: ", response_json.error_string()) -signal document_updated(snapshot : Dictionary) - -const MinPollTime = 60 # seconds +const MinPollTime = 60 * 2 # seconds, so 2 minutes var _doc_name : String -var _tracked_changes : Dictionary var _poll_time : float var _collection : FirestoreCollection -var _timer = Timer.new() var _total_time = 0.0 +var _enabled := false -var _is_requesting_update := false - -func _init(document : FirestoreDocument, poll_time : float) -> void: +func initialize_listener(collection_name : String, doc_name : String, poll_time : float) -> void: _poll_time = max(poll_time, MinPollTime) - _tracked_changes = document._changes.duplicate() - _doc_name = document.doc_name - _collection = Firebase.Firestore.collection(document.collection_name) - - add_child(_timer) + _doc_name = doc_name + _collection = Firebase.Firestore.collection(collection_name) func enable_connection() -> FirestoreListenerConnection: - _timer.wait_time = 1 - _timer.timeout.connect(_update_document, CONNECT_REFERENCE_COUNTED) - _timer.start() + _enabled = true + set_process(true) return FirestoreListenerConnection.new(self) -func _update_document(): - _total_time += _timer.wait_time - var stored_doc = await _collection.get_doc(_doc_name, _total_time <= _poll_time) - if stored_doc != null: - var result = stored_doc._changes - - if _tracked_changes != result: - document_updated.emit(_tracked_changes) - _tracked_changes = result.duplicate() - else: - var result = { - "deleted": { _doc_name: "" } # Not sure if this works, find out what happens when you delete a document and attempt to get it - } - - document_updated.emit(result) - _timer.stop() +func _process(delta: float) -> void: + if _enabled: + _total_time += delta + if _total_time >= _poll_time: + _check_for_server_updates() + _total_time = 0.0 + +func _check_for_server_updates() -> void: + var executor = func(): + var doc = await _collection.get_doc(_doc_name, false, true) + if doc == null: + set_process(false) # Document was deleted out from under us, so stop updating - if _total_time > _poll_time: - _total_time = 0.0 + executor.call() # Hack to work around the await here, otherwise would have to call with await in _process and that's no bueno class FirestoreListenerConnection extends RefCounted: var connection @@ -175,4 +43,5 @@ class FirestoreListenerConnection extends RefCounted: func stop(): if connection != null and is_instance_valid(connection): + connection.set_process(false) connection.free() diff --git a/fonts/PermanentMarker.ttf.import b/fonts/PermanentMarker.ttf.import index abcc65a..c4a5cd1 100644 --- a/fonts/PermanentMarker.ttf.import +++ b/fonts/PermanentMarker.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/PermanentMarker.ttf-47b835cbb05fc10de2652f810 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index d50dc54..424c277 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -14,6 +14,8 @@ const _password : String = 'Password1234' var listener_test_count := 0 +var DefaultDocument = { 'name': 'Document1', 'points': 20, '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.login_succeeded.connect(_on_FirebaseAuth_login_succeeded) @@ -81,7 +83,7 @@ func _test_firestore() -> void: await _cleanup_previous_run() # Add Document1 to Firestore _print_to_console("Trying to add a document") - _document = await _collection.add("Document1", {'name': 'Document1', 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) + _document = await _collection.add("Document1", DefaultDocument) $add_document.button_pressed = true # ## Get Document1 (Document that has been added from the previous step) @@ -98,117 +100,104 @@ func _test_firestore() -> void: _test_error("Failed to get document from cache") return - - # ## Print Document1 to the console GUI - #_print_to_console("Trying to print contents of Document1") - #_print_to_console(_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 up_task = _collection.commit(_document) - #var result = await up_task.commit_document - # - #get_task = _collection.get_doc('Document1') - #_document = await _collection.get_document - # + _print_to_console("Trying to print contents of Document1") + _print_to_console(_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.button_pressed = true - # - #_print_to_console("Attempting to remove item from Document1") - #var key = "name" - #_document.remove_field(key) - # - #_print_to_console("Key erased: " + key) - #_print_to_console("Document now:\n" + str(_document)) - #_print_to_console("\n") - # - #var previous_document_size = _document.keys().size() - # - #var delete_item_from_document_task : FirestoreTask = _collection.update(_document) - #await delete_item_from_document_task.update_document - # - #get_task = _collection.get_doc('Document1') - #_document = await _collection.get_document + _print_to_console("Trying to print contents of Document1 after transform") + _print_to_console(_document) + $print_document_2.button_pressed = true - #_print_to_console("Document now:\n" + str(_document)) - #if previous_document_size == _document.keys().size(): - #_print_to_console_error("Did not properly delete item from document") - # - #_print_to_console("Previous document keys count: " + str(previous_document_size)) - #_print_to_console("Document keys count: " + str(_document.keys().size())) - # - ## Delete Document1 from Firestore - #_print_to_console("Trying to delete Document1") - #var del_task : FirestoreTask = _collection.delete("Document1") - #var deleted = await del_task.delete_document - #$delete_document.button_pressed = true - # - ## 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) - #result = await query_task.result_query - #_print_to_console(result) - #$run_query.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) + + _print_to_console("After update, document key size is: " + str(_document.keys().size())) + + 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 result = await Firebase.Firestore.query(query) + _print_to_console(result) + $run_query.button_pressed = true + _print_to_console("Running listener tests") - #await _cleanup_previous_run() 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() func run_listener_tests() -> void: - #var add_task : FirestoreTask = _collection.add("Document1", {'name': 'Document1', 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) - #_document = await add_task.add_document - _document = await _collection.get_doc("Document1", false) + _document = await _collection.get_doc("Document1") + await get_tree().create_timer(0.3).timeout var listener = _document.on_snapshot( func(result): - print(result) - , .5) # Attempt to update every half second + if result.is_listener: + print(JSON.stringify(result, " ")) + , 0.1 + ) + + print("Doc child count: ", _document.get_child_count(true)) const new_doc_name = 'NewDocument' _document.add_or_update_field("name", new_doc_name) - print("Name updated") - _document = await _collection.update(_document, true) - print("Document updated") + print("Changed name locally") - await get_tree().create_timer(1.0).timeout + await get_tree().create_timer(2.0).timeout _document.add_or_update_field("name", new_doc_name + "2") - print("Updated a second time") + print("Changed name locally again") + + await get_tree().create_timer(2.0).timeout + + _document.add_or_update_field("name", "Document1") + _document.remove_field("active") + + _document = await _collection.update(_document) - await get_tree().create_timer(1.0).timeout var deleted = await _collection.delete(_document) if deleted: print("Deleted document") - await get_tree().create_timer(5.0).timeout - - _document = await _collection.add("Document1", {'name': 'Document1', 'active': 'true', "serviker_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 }) - print("Document re-added") + await get_tree().create_timer(1.0).timeout - print("Listener stopped") 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) From 2371fc50a19730b8b31660db6b4df956a34771c5 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 8 Jun 2024 09:45:45 -0400 Subject: [PATCH 15/22] Storage rewrite (#7) * Storage rewrite so no StorageTask visible * Fix delete and remove valid --- addons/godot-firebase/storage/storage.gd | 71 +++++++------- .../storage/storage_reference.gd | 92 +++++++------------ addons/godot-firebase/storage/storage_task.gd | 47 +++++----- tests/storage/storage.gd | 84 +++++++---------- 4 files changed, 124 insertions(+), 170 deletions(-) diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 82a8017..28d016e 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -10,11 +10,6 @@ extends Node const _API_VERSION : String = "v0" -## @arg-types int, int, PackedStringArray -## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode -## Emitted when a [StorageTask] has finished successful. -signal task_successful(result, response_code, data) - ## @arg-types int, int, PackedStringArray ## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode ## Emitted when a [StorageTask] has finished with an error. @@ -83,6 +78,7 @@ func _internal_process(_delta : float) -> void: 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. @@ -127,13 +123,13 @@ func ref(path := "") -> StorageReference: 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.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] @@ -158,9 +154,9 @@ func _check_emulating() -> void : _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) -func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> StorageTask: +func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> Variant: if _is_invalid_authentication(): - return null + return 0 var task := StorageTask.new() task.ref = ref @@ -169,11 +165,11 @@ func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageR task._headers = headers task.data = data _process_request(task) - return task + return await task.task_finished -func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> StorageTask: +func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Variant: if _is_invalid_authentication(): - return null + return 0 var info_task := StorageTask.new() info_task.ref = ref @@ -182,7 +178,7 @@ func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Sto _process_request(info_task) if url_only or meta_only: - return info_task + return await info_task.task_finished var task := StorageTask.new() task.ref = ref @@ -199,33 +195,36 @@ func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Sto task.response_code = info_task.response_code task.result = info_task.result task.finished = true - task.task_finished.emit() + task.task_finished.emit(null) task_failed.emit(task.result, task.response_code, task.data) _pending_tasks.erase(task) + return null - return task + return await task.task_finished -func _list(ref : StorageReference, list_all : bool) -> StorageTask: +func _list(ref : StorageReference, list_all : bool) -> Array: if _is_invalid_authentication(): - return null + 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 task + return await task.task_finished -func _delete(ref : StorageReference) -> StorageTask: +func _delete(ref : StorageReference) -> bool: if _is_invalid_authentication(): - return null + return false var task := StorageTask.new() task.ref = ref task._url = _get_file_url(ref) task.action = StorageTask.Task.TASK_DELETE _process_request(task) - return task + var data = await task.task_finished + + return data == null func _process_request(task : StorageTask) -> void: if requesting: @@ -262,7 +261,10 @@ func _finish_request(result : int) -> void: StorageTask.Task.TASK_DELETE: _references.erase(task.ref.full_path) - task.ref.valid = false + 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 @@ -301,26 +303,21 @@ func _finish_request(result : int) -> void: var json = Utilities.get_json_data(_response_data) task.data = json - var next_task : StorageTask - if not _pending_tasks.is_empty(): - next_task = _pending_tasks.pop_front() - + 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) - else: - task_successful.emit(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.is_empty(): - next_task = _pending_tasks.pop_front() - else: - break + 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) diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 2ce2446..2989d12 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -4,7 +4,7 @@ ## 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 class_name StorageReference -extends RefCounted +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 checked the file extenstion if none is provided. @@ -36,7 +36,7 @@ const MIME_TYPES = { "txt": "text/plain", "wav": "audio/wav", "webm": "video/webm", - "webp": "video/webm", + "webp": "image/webp", "xml": "text/xml", } @@ -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,25 +64,16 @@ 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.path_join(path)) ## @args data, metadata -## @return StorageTask -## Makes an attempt to upload data to the referenced file location. Status checked this task is found in the returned [StorageTask]. -func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: - if not valid: - return null +## @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() @@ -90,90 +81,75 @@ func put_data(data : PackedByteArray, metadata := {}) -> StorageTask: 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_buffer(), 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: +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) - return put_data(data, metadata) + return await put_data(data, metadata) -## @return StorageTask +## @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() -> StorageTask: - if not valid: - return null - storage._download(self, false, false) - return storage._pending_tasks[-1] +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.task_finished.connect(_on_task_finished.bind(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 checked this task is found in the returned [StorageTask]. -func get_download_url() -> StorageTask: - if not valid: - return null - return storage._download(self, false, true) +func get_download_url() -> Variant: + return await storage._download(self, false, true) ## @return StorageTask ## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask]. -func get_metadata() -> StorageTask: - if not valid: - return null - return storage._download(self, true, false) +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 checked the server end. Status checked this task is found in the returned [StorageTask]. -func update_metadata(metadata : Dictionary) -> StorageTask: - if not valid: - return null +func update_metadata(metadata : Dictionary) -> Variant: var data := JSON.stringify(metadata).to_utf8_buffer() var headers := PackedStringArray(["Accept: application/json"]) - return storage._upload(data, headers, self, true) + 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 checked this task is found in the returned [StorageTask]. -func list() -> StorageTask: - if not valid: - return null - return storage._list(self, false) +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 checked this task is found in the returned [StorageTask]. -func list_all() -> StorageTask: - if not valid: - return null - return storage._list(self, true) +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 checked this task is found in the returned [StorageTask]. -func delete() -> StorageTask: - if not valid: - return null - return storage._delete(self) +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 RefCounted]" return string func _on_task_finished(task : StorageTask, action : String) -> void: diff --git a/addons/godot-firebase/storage/storage_task.gd b/addons/godot-firebase/storage/storage_task.gd index ad810c5..fa5cefd 100644 --- a/addons/godot-firebase/storage/storage_task.gd +++ b/addons/godot-firebase/storage/storage_task.gd @@ -1,4 +1,4 @@ -## @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 @@ -6,23 +6,22 @@ class_name StorageTask 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 checked the success and action of the task. signal task_finished(data) -## @type StorageReference -## The [StorageReference] that created this [StorageTask]. -var ref # Storage RefCounted (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 @@ -30,6 +29,8 @@ var ref # Storage RefCounted (Can't static type due to cyclic reference) ## The kind of operation this [StorageTask] is keeping track of. var action : int = -1 : set = set_action +var ref # Should not be needed, damnit + ## @default PackedByteArray() ## Data that the tracked task will/has returned. var data = PackedByteArray() # data can be of any type. @@ -61,13 +62,13 @@ var _url : String = "" 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/tests/storage/storage.gd b/tests/storage/storage.gd index 3557de8..64f6a60 100644 --- a/tests/storage/storage.gd +++ b/tests/storage/storage.gd @@ -59,120 +59,100 @@ 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") - await upload_task.task_finished + 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') - var data = await 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.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() - await url_task.task_finished - _print_to_console(url_task.data) + 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() - await meta_task.task_finished - _print_to_console(meta_task.data) + 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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - var delete_task = Firebase.Storage.ref("Firebasetester/upload/image.png").delete() - await 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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) + 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") - await upload_task.task_finished + 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() - await meta_task.task_finished - _print_to_console(meta_task.data) + 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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - delete_task = Firebase.Storage.ref("Firebasetester/upload/dummy.pdf").delete() - await 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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) + 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", {}) - await upload_task.task_finished + 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"}) - await meta_task.task_finished + 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() - await meta_task.task_finished - _print_to_console(meta_task.data) + 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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) - delete_task = Firebase.Storage.ref("Firebasetester/upload/junkdata").delete() - await 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() - await list_all_task.task_finished - _print_to_console(list_all_task.data) + 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): + match typeof(vari): TYPE_PACKED_BYTE_ARRAY: - var data : PackedByteArray = task.data + var data : PackedByteArray = vari if data.size()>1: var image_marker = data.slice(0, 1) var hex = image_marker.hex_encode() @@ -182,7 +162,7 @@ func task2image(task : StorageTask) -> ImageTexture: "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 From 685374d521828ceb4f558c5b477fc6b76493f672 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Mon, 10 Jun 2024 06:24:33 -0400 Subject: [PATCH 16/22] Fix issue with _from_firebase_type_recursive (#8) --- addons/godot-firebase/Utilities.gd | 15 ++------------- tests/firestore/firestore.gd | 8 ++++++-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index ed26a71..e828d2a 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -57,24 +57,13 @@ static func from_firebase_type(value : Variant) -> Variant: return null if value.has("mapValue"): - value = _from_firebase_type_recursive(value.values()[0].fields) + value = fields2dict(value.values()[0]) elif value.has("timestampValue"): value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) else: value = value.values()[0] return value - -static func _from_firebase_type_recursive(value : Variant) -> Variant: - if value == null: - return null - - if value.has("mapValue") or value.has("timestampValue"): - value = _from_firebase_type_recursive(value.value()[0].fields) - else: - value = value.values()[0] - - return value static func to_firebase_type(value : Variant) -> Dictionary: var var_type : String = "" @@ -103,7 +92,7 @@ static func fields2dict(doc) -> Dictionary: var dict = {} if doc.has("fields"): var fields = doc["fields"] - print(fields) + for field in fields.keys(): if fields[field].has("mapValue"): dict[field] = (fields2dict(fields[field].mapValue)) diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index 424c277..e27afae 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -14,7 +14,7 @@ const _password : String = 'Password1234' var listener_test_count := 0 -var DefaultDocument = { 'name': 'Document1', 'points': 20, 'active': 'true', "server_timestamp_attempt": null, "increment_field": 0, "decrement_field": 0, "max_field": 5, "min_field": 2 } +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(): @@ -85,7 +85,11 @@ func _test_firestore() -> void: _print_to_console("Trying to add a 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) + ## Get Document1 (Document that has been added from the previous step) _print_to_console("Trying to get Document1") _document = await _collection.get_doc('Document1') From 4e73019bff5b2e6a03959d0ae4e2157fa0a00ba5 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Thu, 27 Jun 2024 06:41:25 -0400 Subject: [PATCH 17/22] Update Firestore fields for correct types --- addons/godot-firebase/Utilities.gd | 38 ++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/addons/godot-firebase/Utilities.gd b/addons/godot-firebase/Utilities.gd index e828d2a..13f4101 100644 --- a/addons/godot-firebase/Utilities.gd +++ b/addons/godot-firebase/Utilities.gd @@ -52,19 +52,49 @@ static func dict2fields(dict : Dictionary) -> Dictionary: return {'fields' : fields} -static func from_firebase_type(value : Variant) -> Variant: +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: - value = value.values()[0] - + 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 = "" From c0dbe8efaf03f780e2545ed5582df5af8d0c53ac Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Fri, 19 Jul 2024 08:27:15 -0400 Subject: [PATCH 18/22] Firestore query updates (#11) * Add aggregation queries * Fix firestore doc typo --- addons/godot-firebase/firestore/firestore.gd | 137 +++++++----------- .../firestore/firestore_query.gd | 39 +++-- .../firestore/firestore_task.gd | 41 ++++-- tests/firestore/firestore.gd | 44 +++++- 4 files changed, 149 insertions(+), 112 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 772a228..f7ea76e 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -40,39 +40,27 @@ 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 = false - -## Whether an internet connection can be used. -## @default true -var networking: bool = true : set = set_networking +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 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/(default)/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 _offline: bool = false : set = _set_offline - -func _ready() -> void: - pass +var _request_list_node: HTTPRequest +var _requests_queue: Array = [] +var _current_query: FirestoreQuery ## Returns a reference collection by its [i]path[/i]. ## @@ -99,49 +87,69 @@ func collection(path : String) -> FirestoreCollection: ## 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 checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. -## -## ex. -## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code] -## [code]await 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. +## When awaited, this function returns the resulting array from the query. ## ## ex. -## [code]var result : Array = await 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 +## @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_suffix - + var body: Dictionary = { structuredQuery = query.query } + var url: String = _base_url + _extended_url + _query_suffix + task.data = query task._fields = JSON.stringify(body) task._url = url _pooled_request(task) return await _handle_task_finished(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 checked 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]await 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. + +## 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 = await 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 +## @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 @@ -160,38 +168,6 @@ func list(path : String = "", page_size : int = 0, page_token : String = "", ord return await _handle_task_finished(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 coll in get_children(): - if coll is FirestoreCollection: - coll._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 coll in get_children(): - if coll is FirestoreCollection: - coll._base_url = _base_url - - -func _set_offline(value: bool) -> void: - return # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted. - - func _set_config(config_json : Dictionary) -> void: _config = config_json _cache_loc = _config["cacheLocation"] @@ -213,10 +189,6 @@ func _check_emulating() -> void : _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, PackedStringArray(), PackedByteArray()) - return - if (auth == null or auth.is_empty()) and not Firebase.emulating: Firebase._print("Unauthenticated request issued...") Firebase.Auth.login_anonymous() @@ -252,11 +224,6 @@ func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: if coll is FirestoreCollection: coll.auth = auth -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) - - func _on_FirebaseAuth_logout() -> void: auth = {} diff --git a/addons/godot-firebase/firestore/firestore_query.gd b/addons/godot-firebase/firestore/firestore_query.gd index 6e4b764..bdebbe6 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -1,4 +1,4 @@ -## @meta-authors Nicoló 'fenix' Santilio +## @meta-authors Nicoló 'fenix' Santilio, Kyle Szklenski ## @meta-version 1.4 ## A firestore query. ## Documentation TODO. @@ -7,11 +7,11 @@ 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 @@ -19,7 +19,7 @@ class Cursor: signal query_result(query_result) -const TEMPLATE_QUERY : Dictionary = { +const TEMPLATE_QUERY: Dictionary = { select = {}, from = [], where = {}, @@ -30,11 +30,12 @@ const TEMPLATE_QUERY : Dictionary = { limit = 0 } -var query : Dictionary = {} +var query: Dictionary = {} +var aggregations: Array[Dictionary] = [] enum OPERATOR { # Standard operators - OPERATOR_NSPECIFIED, + OPERATOR_UNSPECIFIED, LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, @@ -87,8 +88,6 @@ func from(collection_id : String, all_descendants : bool = true) -> FirestoreQue 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: @@ -159,8 +158,6 @@ func order_by_fields(order_field_list : Array) -> FirestoreQuery: 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 } @@ -191,6 +188,26 @@ func limit(limit : int) -> FirestoreQuery: 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 ---------------------------------------- diff --git a/addons/godot-firebase/firestore/firestore_task.gd b/addons/godot-firebase/firestore/firestore_task.gd index 9fad023..8122ae5 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -31,6 +31,7 @@ enum Task { 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 } @@ -43,7 +44,8 @@ const TASK_MAP = { Task.TASK_DELETE: "DELETE DOCUMENT", Task.TASK_QUERY: "QUERY COLLECTION", Task.TASK_LIST: "LIST DOCUMENTS", - Task.TASK_COMMIT: "COMMIT DOCUMENT" + Task.TASK_COMMIT: "COMMIT DOCUMENT", + Task.TASK_AGG_QUERY: "AGG QUERY COLLECTION" } ## The code indicating the request Firestore is processing. @@ -53,24 +55,23 @@ var action : int = -1 : set = set_action ## A variable, temporary holding the result of the request. var data -var error : Dictionary -var document : FirestoreDocument +var error: Dictionary +var document: FirestoreDocument -var _response_headers : PackedStringArray = PackedStringArray() -var _response_code : int = 0 +var _response_headers: PackedStringArray = PackedStringArray() +var _response_code: int = 0 -var _method : int = -1 -var _url : String = "" -var _fields : String = "" -var _headers : PackedStringArray = [] +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: +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: @@ -84,6 +85,18 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt 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'): @@ -127,7 +140,7 @@ func set_action(value : int) -> void: match action: Task.TASK_GET, Task.TASK_LIST: _method = HTTPClient.METHOD_GET - Task.TASK_POST, Task.TASK_QUERY: + Task.TASK_POST, Task.TASK_QUERY, Task.TASK_AGG_QUERY: _method = HTTPClient.METHOD_POST Task.TASK_PATCH: _method = HTTPClient.METHOD_PATCH @@ -135,6 +148,8 @@ func set_action(value : int) -> void: _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: diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index e27afae..529b234 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -152,11 +152,49 @@ func _test_firestore() -> void: query.limit(10) var result = await Firebase.Firestore.query(query) _print_to_console(result) + + ## 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 - - _print_to_console("Running listener tests") - await run_listener_tests() + ## 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 From dc8c0bb7afa444cb751d16e76f85c9cf10f1ef39 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Fri, 19 Jul 2024 11:00:56 -0400 Subject: [PATCH 19/22] Fix null ref issue --- addons/godot-firebase/firestore/firestore_collection.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore_collection.gd b/addons/godot-firebase/firestore/firestore_collection.gd index 0dae95f..4883f87 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -108,8 +108,8 @@ func update(document : FirestoreDocument) -> FirestoreDocument: child.replace(result, true) break - if temp_transforms != null: - result._transforms = temp_transforms + if temp_transforms != null: + result._transforms = temp_transforms return result From 120f06f8db7f52d1a8cd0074cd3e95e7412c041f Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 19 Oct 2024 12:39:31 -0400 Subject: [PATCH 20/22] Add getter and unsafe documents, fix replace --- .../firestore/firestore_document.gd | 43 ++++++++++++++++++- addons/gut/icon.png.import | 35 --------------- project.godot | 4 ++ tests/firestore/firestore.gd | 14 ++++-- 4 files changed, 56 insertions(+), 40 deletions(-) delete mode 100644 addons/gut/icon.png.import diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index f0f9249..d99ac01 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -42,7 +42,36 @@ func replace(with : FirestoreDocument, is_listener := false) -> void: else: var new_value = Utilities.from_firebase_type(document[key]) var old_value = Utilities.from_firebase_type(current[key]) - if new_value != old_value: + if typeof(new_value) != typeof(old_value) or new_value != old_value: + if old_value == null: + changes.removed.push_back({ "key" : key }) # ?? + else: + 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()): + changed.emit(changes) + +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": [], "is_listener": false + } + + 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: if old_value == null: changes.removed.push_back({ "key" : key }) # ?? else: @@ -124,15 +153,25 @@ func get_value(property : StringName) -> Variant: if document.has(property): var result = Utilities.from_firebase_type(document[property]) - return result return null +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() diff --git a/addons/gut/icon.png.import b/addons/gut/icon.png.import deleted file mode 100644 index 3f2dabe..0000000 --- a/addons/gut/icon.png.import +++ /dev/null @@ -1,35 +0,0 @@ -[remap] - -importer="texture" -type="StreamTexture" -path="res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://addons/gut/icon.png" -dest_files=[ "res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" ] - -[params] - -compress/mode=0 -compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 -compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 -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 -svg/scale=1.0 diff --git a/project.godot b/project.godot index 3b1271f..0a3a036 100644 --- a/project.godot +++ b/project.godot @@ -19,6 +19,10 @@ config/icon="res://icon.png" Firebase="*res://addons/godot-firebase/firebase/firebase.tscn" +[display] + +window/dpi/allow_hidpi=false + [editor_plugins] enabled=PackedStringArray("res://addons/godot-firebase/plugin.cfg", "res://addons/http-sse-client/plugin.cfg") diff --git a/tests/firestore/firestore.gd b/tests/firestore/firestore.gd index 529b234..a530bb6 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -90,6 +90,14 @@ func _test_firestore() -> void: 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') @@ -106,10 +114,10 @@ func _test_firestore() -> void: # ## Print Document1 to the console GUI - _print_to_console("Trying to print contents of Document1") - _print_to_console(_document) + _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) From a95b076ed23e49824cc92f48f6942ceaa3782996 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Sat, 19 Oct 2024 13:16:15 -0400 Subject: [PATCH 21/22] Update for basic checks --- .../firestore/firestore_document.gd | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/addons/godot-firebase/firestore/firestore_document.gd b/addons/godot-firebase/firestore/firestore_document.gd index d99ac01..86d2459 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -1,4 +1,4 @@ -## @meta-authors TODO +## @meta-authors Kyle Szklenski ## @meta-version 2.2 ## A reference to a Firestore Document. ## Documentation TODO. @@ -21,12 +21,14 @@ signal changed(changes) func _init(doc : Dictionary = {}): _transforms = FieldTransformArray.new() - document = doc.fields - doc_name = doc.name - if doc_name.count("/") > 2: - doc_name = (doc_name.split("/") as Array).back() - - self.create_time = doc.createTime + 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 func replace(with : FirestoreDocument, is_listener := false) -> void: var current = document.duplicate() From 65497729bb7cdfe626745b5363b0f94915f76b32 Mon Sep 17 00:00:00 2001 From: Kyle Szklenski Date: Wed, 26 Nov 2025 07:24:21 -0500 Subject: [PATCH 22/22] Update test harness --- addons/godot-firebase/Utilities.gd.uid | 1 + addons/godot-firebase/auth/auth.gd.uid | 1 + .../godot-firebase/auth/auth_provider.gd.uid | 1 + .../auth/providers/facebook.gd.uid | 1 + .../auth/providers/github.gd.uid | 1 + .../auth/providers/google.gd.uid | 1 + .../auth/providers/twitter.gd.uid | 1 + addons/godot-firebase/auth/user_data.gd.uid | 1 + .../godot-firebase/database/database.gd.uid | 1 + .../database/database_store.gd.uid | 1 + .../database/firebase_database_reference.tscn | 6 +- .../godot-firebase/database/once_reference.gd | 80 +++++-- .../database/once_reference.gd.uid | 1 + addons/godot-firebase/database/reference.gd | 42 +++- .../godot-firebase/database/reference.gd.uid | 1 + .../godot-firebase/database/resource.gd.uid | 1 + .../dynamiclinks/dynamiclinks.gd.uid | 1 + .../godot-firebase/firebase/firebase.gd.uid | 1 + .../firestore/field_transform.gd.uid | 1 + .../firestore/field_transform_array.gd.uid | 1 + .../decrement_transform.gd.uid | 1 + .../increment_transform.gd.uid | 1 + .../field_transforms/max_transform.gd.uid | 1 + .../field_transforms/min_transform.gd.uid | 1 + .../server_timestamp_transform.gd.uid | 1 + addons/godot-firebase/firestore/firestore.gd | 9 +- .../godot-firebase/firestore/firestore.gd.uid | 1 + .../firestore/firestore_collection.gd | 45 +++- .../firestore/firestore_collection.gd.uid | 1 + .../firestore/firestore_document.gd | 42 ++-- .../firestore/firestore_document.gd.uid | 1 + .../firestore/firestore_listener.gd | 144 +++++++++--- .../firestore/firestore_listener.gd.uid | 1 + .../firestore/firestore_listener.tscn | 2 +- .../firestore/firestore_query.gd.uid | 1 + .../firestore/firestore_task.gd.uid | 1 + .../firestore/firestore_transform.gd.uid | 1 + .../functions/function_task.gd.uid | 1 + .../godot-firebase/functions/functions.gd.uid | 1 + addons/godot-firebase/plugin.gd | 8 +- addons/godot-firebase/plugin.gd.uid | 1 + .../queues/queueable_http_request.gd.uid | 1 + .../firebase_remote_config.gd.uid | 1 + .../remote_config/remote_config.gd.uid | 1 + addons/godot-firebase/storage/storage.gd.uid | 1 + .../storage/storage_reference.gd.uid | 1 + .../storage/storage_task.gd.uid | 1 + addons/http-sse-client/HTTPSSEClient.gd | 107 ++++++--- addons/http-sse-client/HTTPSSEClient.gd.uid | 1 + .../httpsseclient_plugin.gd.uid | 1 + assets/buttons/normal_button.gd.uid | 1 + fonts/PermanentMarker.ttf.import | 2 + main.gd | 2 +- main.gd.uid | 1 + main.tscn | 34 ++- project.godot | 4 +- tests/auth/auth.gd | 2 +- tests/auth/auth.gd.uid | 1 + tests/auth/auth.tscn | 19 +- tests/database/database.gd | 4 +- tests/database/database.gd.uid | 1 + tests/database/database.tscn | 19 +- tests/database/test_etag.gd | 51 ++++ tests/database/test_etag.gd.uid | 1 + tests/firestore/firestore.gd | 156 ++++++++++++- tests/firestore/firestore.gd.uid | 1 + tests/firestore/firestore.tscn | 103 ++++++++- tests/links/links.gd | 112 ++++----- tests/links/links.gd.uid | 1 + tests/links/links.tscn | 82 +++---- tests/remote_config/remote_config.gd.uid | 1 + tests/remote_config/remote_config.tscn | 1 + tests/storage/storage.gd | 2 +- tests/storage/storage.gd.uid | 1 + tests/storage/storage.tscn | 217 +++++++----------- 75 files changed, 958 insertions(+), 386 deletions(-) create mode 100644 addons/godot-firebase/Utilities.gd.uid create mode 100644 addons/godot-firebase/auth/auth.gd.uid create mode 100644 addons/godot-firebase/auth/auth_provider.gd.uid create mode 100644 addons/godot-firebase/auth/providers/facebook.gd.uid create mode 100644 addons/godot-firebase/auth/providers/github.gd.uid create mode 100644 addons/godot-firebase/auth/providers/google.gd.uid create mode 100644 addons/godot-firebase/auth/providers/twitter.gd.uid create mode 100644 addons/godot-firebase/auth/user_data.gd.uid create mode 100644 addons/godot-firebase/database/database.gd.uid create mode 100644 addons/godot-firebase/database/database_store.gd.uid create mode 100644 addons/godot-firebase/database/once_reference.gd.uid create mode 100644 addons/godot-firebase/database/reference.gd.uid create mode 100644 addons/godot-firebase/database/resource.gd.uid create mode 100644 addons/godot-firebase/dynamiclinks/dynamiclinks.gd.uid create mode 100644 addons/godot-firebase/firebase/firebase.gd.uid create mode 100644 addons/godot-firebase/firestore/field_transform.gd.uid create mode 100644 addons/godot-firebase/firestore/field_transform_array.gd.uid create mode 100644 addons/godot-firebase/firestore/field_transforms/decrement_transform.gd.uid create mode 100644 addons/godot-firebase/firestore/field_transforms/increment_transform.gd.uid create mode 100644 addons/godot-firebase/firestore/field_transforms/max_transform.gd.uid create mode 100644 addons/godot-firebase/firestore/field_transforms/min_transform.gd.uid create mode 100644 addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd.uid create mode 100644 addons/godot-firebase/firestore/firestore.gd.uid create mode 100644 addons/godot-firebase/firestore/firestore_collection.gd.uid create mode 100644 addons/godot-firebase/firestore/firestore_document.gd.uid create mode 100644 addons/godot-firebase/firestore/firestore_listener.gd.uid create mode 100644 addons/godot-firebase/firestore/firestore_query.gd.uid create mode 100644 addons/godot-firebase/firestore/firestore_task.gd.uid create mode 100644 addons/godot-firebase/firestore/firestore_transform.gd.uid create mode 100644 addons/godot-firebase/functions/function_task.gd.uid create mode 100644 addons/godot-firebase/functions/functions.gd.uid create mode 100644 addons/godot-firebase/plugin.gd.uid create mode 100644 addons/godot-firebase/queues/queueable_http_request.gd.uid create mode 100644 addons/godot-firebase/remote_config/firebase_remote_config.gd.uid create mode 100644 addons/godot-firebase/remote_config/remote_config.gd.uid create mode 100644 addons/godot-firebase/storage/storage.gd.uid create mode 100644 addons/godot-firebase/storage/storage_reference.gd.uid create mode 100644 addons/godot-firebase/storage/storage_task.gd.uid create mode 100644 addons/http-sse-client/HTTPSSEClient.gd.uid create mode 100644 addons/http-sse-client/httpsseclient_plugin.gd.uid create mode 100644 assets/buttons/normal_button.gd.uid create mode 100644 main.gd.uid create mode 100644 tests/auth/auth.gd.uid create mode 100644 tests/database/database.gd.uid create mode 100644 tests/database/test_etag.gd create mode 100644 tests/database/test_etag.gd.uid create mode 100644 tests/firestore/firestore.gd.uid create mode 100644 tests/links/links.gd.uid create mode 100644 tests/remote_config/remote_config.gd.uid create mode 100644 tests/storage/storage.gd.uid 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.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.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.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.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.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.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.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.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.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 index 27abf89..3a74025 100644 --- a/addons/godot-firebase/database/firebase_database_reference.tscn +++ b/addons/godot-firebase/database/firebase_database_reference.tscn @@ -1,9 +1,9 @@ [gd_scene load_steps=5 format=3 uid="uid://btltp52tywbe4"] -[ext_resource type="Script" path="res://addons/godot-firebase/database/reference.gd" id="1_l3oy5"] +[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" path="res://addons/http-sse-client/HTTPSSEClient.gd" id="2_4l0io"] -[ext_resource type="Script" path="res://addons/godot-firebase/database/database_store.gd" id="3_c3r2w"] +[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") diff --git a/addons/godot-firebase/database/once_reference.gd b/addons/godot-firebase/database/once_reference.gd index ac816e4..ed9f7d0 100644 --- a/addons/godot-firebase/database/once_reference.gd +++ b/addons/godot-firebase/database/once_reference.gd @@ -27,6 +27,7 @@ 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" @@ -41,6 +42,7 @@ 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 @@ -54,8 +56,11 @@ func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void # 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() - _oncer.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") + 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 = "" @@ -79,25 +84,38 @@ func _get_list_url(with_port:bool = true) -> String: func _get_filter(): if _filter_query_empty(): return "" - - var filter = "" - - if _filter_query.has(ORDER_BY): - filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote - _filter_query.erase(ORDER_BY) + # 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: - 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(): - filter += _filter_tag + key + _equal_tag + _filter_query[key] + 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 filter + 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: @@ -113,7 +131,7 @@ 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) -> void: +func update(path : String, data : Dictionary, etag : String = "") -> void: path = path.strip_edges(true, true) if path == _separator: @@ -121,4 +139,38 @@ func update(path : String, data : Dictionary) -> void: var to_update = JSON.stringify(data) var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) - _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) + + 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 0429b92..4adc864 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -91,7 +91,7 @@ func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) - elif command == _delete_tag: delete_data_update.emit(FirebaseResource.new(data.path, data.data)) -func update(path : String, data : Dictionary) -> void: +func update(path : String, data : Dictionary, etag : String = "") -> void: path = path.strip_edges(true, true) if path == _separator: @@ -100,14 +100,40 @@ func update(path : String, data : Dictionary) -> void: var to_update = JSON.stringify(data) var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) - _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) + + 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.stringify(data) _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) -func delete(reference : String) -> void: - _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") +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 @@ -151,6 +177,14 @@ func _get_filter(): _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 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.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.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/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/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.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.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.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.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.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.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 f7ea76e..be97605 100644 --- a/addons/godot-firebase/firestore/firestore.gd +++ b/addons/godot-firebase/firestore/firestore.gd @@ -52,7 +52,7 @@ var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_uni var _base_url: String = "" -var _extended_url: String = "projects/[PROJECT_ID]/databases/(default)/documents/" +var _extended_url: String = "projects/{PROJECT_ID}/databases/{DATABASE_NAME}/documents/" var _query_suffix: String = ":runQuery" var _agg_query_suffix: String = ":runAggregationQuery" @@ -105,7 +105,7 @@ func query(query : FirestoreQuery) -> Array: 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_suffix + var url: String = _base_url + _extended_url + query.sub_collection_path + _query_suffix task.data = query task._fields = JSON.stringify(body) @@ -171,8 +171,9 @@ func list(path : String = "", page_size : int = 0, page_token : String = "", ord func _set_config(config_json : Dictionary) -> void: _config = config_json _cache_loc = _config["cacheLocation"] - _extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId) - + 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() 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 4883f87..7c73f69 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -44,11 +44,18 @@ func get_doc(document_id : String, from_cache : bool = false, is_listener : bool _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]) @@ -107,6 +114,7 @@ func update(document : FirestoreDocument) -> FirestoreDocument: 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 @@ -130,10 +138,25 @@ func commit(document : FirestoreDocument) -> Dictionary: ) # 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) - return await Firebase.Firestore._handle_task_finished(task) # Not implementing the follow-up get here as user may have a listener that's already listening for changes, but user should call get if they don't + 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 @@ -176,3 +199,23 @@ func _process_request(task : FirestoreTask, document_id : String, url : String, 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 86d2459..23a37ab 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -35,7 +35,7 @@ func replace(with : FirestoreDocument, is_listener := false) -> void: document = with.document var changes = { - "added": [], "removed": [], "updated": [], "is_listener": is_listener + "added": [], "removed": [], "updated": [] } for key in current.keys(): @@ -45,16 +45,24 @@ func replace(with : FirestoreDocument, is_listener := false) -> void: 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: - if old_value == null: - changes.removed.push_back({ "key" : key }) # ?? - else: - changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_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) func new_document(base_document: Dictionary) -> void: @@ -64,7 +72,7 @@ func new_document(base_document: Dictionary) -> void: document[key] = Utilities.to_firebase_type(key) var changes = { - "added": [], "removed": [], "updated": [], "is_listener": false + "added": [], "removed": [], "updated": [] } for key in current.keys(): @@ -74,17 +82,14 @@ func new_document(base_document: Dictionary) -> void: 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: - if old_value == null: - changes.removed.push_back({ "key" : key }) # ?? - else: - changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_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()): - changed.emit(changes) + _emit_changes(changes) func is_null_value(key) -> bool: return document.has(key) and Utilities.from_firebase_type(document[key]) == null @@ -92,6 +97,7 @@ func is_null_value(key) -> bool: # 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) func remove_field_transform(transform : FieldTransform) -> void: @@ -105,18 +111,18 @@ func remove_field(field_path : String) -> void: document[field_path] = Utilities.to_firebase_type(null) var changes = { - "added": [], "removed": [], "updated": [], "is_listener": false + "added": [], "removed": [], "updated": [] } changes.removed.push_back({ "key" : field_path }) - changed.emit(changes) + _emit_changes(changes) func _erase(field_path : String) -> void: document.erase(field_path) func add_or_update_field(field_path : String, value : Variant) -> void: var changes = { - "added": [], "removed": [], "updated": [], "is_listener": false + "added": [], "removed": [], "updated": [] } var existing_value = get_value(field_path) @@ -129,19 +135,19 @@ func add_or_update_field(field_path : String, value : Variant) -> void: changes.updated.push_back({ "key" : field_path, "old" : existing_value, "new" : value }) else: changes.added.push_back({ "key" : field_path, "new" : value }) - - changed.emit(changes) + + _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 - changed.connect(when_called, CONNECT_REFERENCE_COUNTED) var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate() add_child(listener) - listener.initialize_listener(collection_name, doc_name, poll_time) + 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 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 index 7808a5c..01c4286 100644 --- a/addons/godot-firebase/firestore/firestore_listener.gd +++ b/addons/godot-firebase/firestore/firestore_listener.gd @@ -1,39 +1,126 @@ class_name FirestoreListener extends Node -const MinPollTime = 60 * 2 # seconds, so 2 minutes +signal changed(changes) -var _doc_name : String -var _poll_time : float -var _collection : FirestoreCollection +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 -var _total_time = 0.0 -var _enabled := false - -func initialize_listener(collection_name : String, doc_name : String, poll_time : float) -> void: - _poll_time = max(poll_time, MinPollTime) +func initialize_listener(collection_name: String, doc_name: String) -> void: _doc_name = doc_name - _collection = Firebase.Firestore.collection(collection_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, {}) -func enable_connection() -> FirestoreListenerConnection: - _enabled = true - set_process(true) - return FirestoreListenerConnection.new(self) + # 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 _process(delta: float) -> void: - if _enabled: - _total_time += delta - if _total_time >= _poll_time: - _check_for_server_updates() - _total_time = 0.0 -func _check_for_server_updates() -> void: - var executor = func(): - var doc = await _collection.get_doc(_doc_name, false, true) - if doc == null: - set_process(false) # Document was deleted out from under us, so stop updating +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 - executor.call() # Hack to work around the await here, otherwise would have to call with await in _process and that's no bueno + # 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 @@ -43,5 +130,8 @@ class FirestoreListenerConnection extends RefCounted: func stop(): if connection != null and is_instance_valid(connection): - connection.set_process(false) 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 index 9f5e246..db467d4 100644 --- a/addons/godot-firebase/firestore/firestore_listener.tscn +++ b/addons/godot-firebase/firestore/firestore_listener.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://bwv7vtgssc0n5"] -[ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore_listener.gd" id="1_qlaei"] +[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.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.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.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.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.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/plugin.gd b/addons/godot-firebase/plugin.gd index f68d5ae..de32c1a 100644 --- a/addons/godot-firebase/plugin.gd +++ b/addons/godot-firebase/plugin.gd @@ -1,8 +1,8 @@ @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.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/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/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.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.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.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 9906de9..6704c46 100644 --- a/addons/http-sse-client/HTTPSSEClient.gd +++ b/addons/http-sse-client/HTTPSSEClient.gd @@ -49,20 +49,20 @@ func attempt_to_request(httpclient_status): 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 _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: @@ -103,26 +103,65 @@ func _process(delta): response_body.append_array(data[1]) if response_body.size() > 0: _parse_response_body(headers) - -func get_event_data(body : String): - 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) - var event_value = event.replace(event_tag, "").strip_edges() - assert(event_value) - assert(event_value.length() > 0) - result["event"] = event_value - var data = body.right(body.length() - (data_idx + data_tag.length())).strip_edges() - assert(data) - assert(data.length() > 0) - result["data"] = data - return result + +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: 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.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/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/fonts/PermanentMarker.ttf.import b/fonts/PermanentMarker.ttf.import index c4a5cd1..d47330d 100644 --- a/fonts/PermanentMarker.ttf.import +++ b/fonts/PermanentMarker.ttf.import @@ -21,8 +21,10 @@ 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=[] diff --git a/main.gd b/main.gd index 293408e..ba67a98 100644 --- a/main.gd +++ b/main.gd @@ -1,4 +1,4 @@ -extends Node2D +extends Control func _ready(): pass 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 e68bfd2..6d86660 100644 --- a/main.tscn +++ b/main.tscn @@ -5,20 +5,32 @@ [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"] +[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="."] -offset_right = 1024.0 -offset_bottom = 600.0 +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 -offset_right = 1024.0 -offset_bottom = 600.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_horizontal = 3 size_flags_vertical = 3 @@ -81,11 +93,23 @@ label = "Database" 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"] layout_mode = 2 size_flags_vertical = 3 [node name="title" type="Label" parent="."] +layout_mode = 0 offset_left = 40.0 offset_top = 49.009 offset_right = 412.0 diff --git a/project.godot b/project.godot index 0a3a036..7ce5453 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="FirebaseTestHarness" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.3") +config/features=PackedStringArray("4.5") config/icon="res://icon.png" [autoload] @@ -21,6 +21,7 @@ Firebase="*res://addons/godot-firebase/firebase/firebase.tscn" [display] +window/stretch/mode="canvas_items" window/dpi/allow_hidpi=false [editor_plugins] @@ -35,4 +36,5 @@ common/enable_pause_aware_picking=true 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 1a31288..fddb029 100644 --- a/tests/auth/auth.gd +++ b/tests/auth/auth.gd @@ -1,4 +1,4 @@ -extends Node2D +extends Control # Script used for testing the Authentication functions of the plugin 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 9b691fa..980319c 100644 --- a/tests/auth/auth.tscn +++ b/tests/auth/auth.tscn @@ -4,12 +4,23 @@ [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="Node2D"] +[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="."] -offset_right = 1024.0 -offset_bottom = 600.0 +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="."] @@ -22,11 +33,13 @@ horizontal_alignment = 1 uppercase = true [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")] +layout_mode = 1 offset_left = 0.0 offset_top = 65.0 offset_right = 190.0 diff --git a/tests/database/database.gd b/tests/database/database.gd index cf9eaaa..95266f4 100644 --- a/tests/database/database.gd +++ b/tests/database/database.gd @@ -1,4 +1,4 @@ -extends Node2D +extends Control # Script used for testing the Realtime Database functions of the plugin signal database_call_completed @@ -90,7 +90,7 @@ func _test_database(): # 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 failed") + _print_to_console("Once called") $once_data_check.button_pressed = true # Update data in the RTDB _print_to_console("\nTrying to update the DB") 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 12a8194..4ce4948 100644 --- a/tests/database/database.tscn +++ b/tests/database/database.tscn @@ -7,12 +7,23 @@ [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="Node2D"] +[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="."] -offset_right = 1024.0 -offset_bottom = 600.0 +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="."] @@ -24,11 +35,13 @@ text = "Database Tests" uppercase = true [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 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 a530bb6..cd1761b 100644 --- a/tests/firestore/firestore.gd +++ b/tests/firestore/firestore.gd @@ -1,4 +1,4 @@ -extends Node2D +extends Control # Script used for testing the Firestore functions of the plugin @@ -7,11 +7,16 @@ extends Node2D @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 } @@ -31,6 +36,16 @@ func _test_started() -> void: 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 @@ -38,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 @@ -56,7 +79,11 @@ func _cleanup_previous_run(): func _on_FirebaseAuth_login_succeeded(_auth) -> void: _print_to_console("Login with email and password has worked") $login_check.button_pressed = true - await _test_firestore() + + 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 @@ -68,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 @@ -201,8 +325,8 @@ func _test_firestore() -> void: $run_query.button_pressed = true - #_print_to_console("Running listener tests") - #await run_listener_tests() + _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 @@ -214,14 +338,23 @@ func run_listener_tests() -> void: await get_tree().create_timer(0.3).timeout var listener = _document.on_snapshot( - func(result): - if result.is_listener: - print(JSON.stringify(result, " ")) - , 0.1 + 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("================================") ) - - print("Doc child count: ", _document.get_child_count(true)) - + + const new_doc_name = 'NewDocument' _document.add_or_update_field("name", new_doc_name) @@ -235,6 +368,7 @@ func run_listener_tests() -> void: _document.add_or_update_field("name", "Document1") _document.remove_field("active") + print("Changed name locally and removed active") _document = await _collection.update(_document) 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 321b2ca..acf4dce 100644 --- a/tests/firestore/firestore.tscn +++ b/tests/firestore/firestore.tscn @@ -1,23 +1,33 @@ [gd_scene load_steps=7 format=3 uid="uid://by5qpv1a7wc8n"] -[ext_resource type="Script" path="res://tests/firestore/firestore.gd" id="1"] +[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" path="res://assets/buttons/normal_button.gd" id="7"] +[ext_resource type="Script" uid="uid://bjg3j1t8bgcb4" path="res://assets/buttons/normal_button.gd" id="7"] -[node name="firestore" type="Node2D"] +[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="."] -offset_left = 5.59372 -offset_top = 2.00226 -offset_right = 1029.59 -offset_bottom = 602.002 +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="."] +layout_mode = 0 offset_left = 376.0 offset_top = 11.8108 offset_right = 557.0 @@ -27,17 +37,21 @@ horizontal_alignment = 1 uppercase = true [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 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") @@ -51,7 +65,67 @@ 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 +bbcode_enabled = 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="."] +layout_mode = 0 offset_left = 249.251 offset_top = 117.012 offset_right = 945.251 @@ -60,6 +134,7 @@ bbcode_enabled = true scroll_following = true [node name="login_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 131.0 offset_right = 34.0 @@ -77,6 +152,7 @@ offset_bottom = 22.9351 text = "Login" [node name="add_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 155.0 offset_right = 34.0 @@ -94,6 +170,7 @@ offset_bottom = 22.9351 text = "Add Document" [node name="get_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 180.0 offset_right = 34.0 @@ -111,6 +188,7 @@ offset_bottom = 22.9351 text = "Get Document" [node name="print_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 203.0 offset_right = 34.0 @@ -128,6 +206,7 @@ offset_bottom = 22.9351 text = "Print Document" [node name="update_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 224.0 offset_right = 34.0 @@ -146,6 +225,7 @@ text = "Update Document" [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 @@ -164,6 +244,7 @@ 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 @@ -181,6 +262,7 @@ offset_bottom = 22.9351 text = "Get Document" [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 @@ -198,6 +280,7 @@ offset_bottom = 22.9351 text = "Print Document" [node name="delete_document" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 314.0 offset_right = 34.0 @@ -215,6 +298,7 @@ offset_bottom = 22.9351 text = "Delete Document" [node name="run_query" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 338.0 offset_right = 34.0 @@ -233,3 +317,8 @@ 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 075d549..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 @@ -15,97 +15,97 @@ const _email : String = 'testaccount@godotnuts.test' const _password : String = 'Password1234' func _ready(): - Firebase.Auth.connect("login_succeeded",Callable(self,"_on_FirebaseAuth_login_succeeded")) - Firebase.Auth.connect("login_failed",Callable(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 # 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_links.disabled = true + _test_running = true + var checkboxes = get_tree().get_nodes_in_group('tests') + for box in checkboxes: + box.button_pressed = false + $back.disabled = true + $test_links.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_links.disabled = false + _test_running = false + $back.disabled = false + $test_links.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_links() + _print_to_console("Login with email and password has worked") + $login_check.button_pressed = true + _test_links() # 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)) - _test_error("Login Failed") + _print_to_console_error("error code: " + str(error_code)) + _print_to_console_error("message: " + str(message)) + _test_error("Login Failed") # Function called if there is an error in the test # Prints the error to the GUI console so the end user can see func _test_error(data) -> void: - _print_to_console_error(data) - _test_finished() + _print_to_console_error(data) + _test_finished() # Function called when the end user presses the 'Test Links' button # Starts the test func _on_test_links_pressed(): - _test_started() - Firebase.Auth.login_with_email_and_password(_email, _password) + _test_started() + Firebase.Auth.login_with_email_and_password(_email, _password) # Main function that is run when testing Dynamic Links func _test_links(): - # Print to the console GUI that the test is starting - _print_to_console("STARTING LINKS TESTS") - - # Connect to signals needed for testing - 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) - 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) - 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") - _test_finished() + # Print to the console GUI that the test is starting + _print_to_console("STARTING LINKS TESTS") + + # Connect to signals needed for testing + 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) + 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) + 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") + _test_finished() # Function used to print the link data to the console GUI func print_link(link_data): - _print_to_console(link_data) - emit_signal('link_printed') + _print_to_console(link_data) + emit_signal('link_printed') # 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 + 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 + 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") + 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 cbd795a..d324a68 100644 --- a/tests/links/links.tscn +++ b/tests/links/links.tscn @@ -1,61 +1,62 @@ -[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="FontFile" id=2] -[ext_resource path="res://assets/background.png" type="Texture2D" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" 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="."] -offset_right = 1024.0 -offset_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="."] +layout_mode = 0 offset_left = 376.0 offset_top = 13.0 offset_right = 498.0 offset_bottom = 52.0 -custom_fonts/font = ExtResource( 4 ) text = "Links Tests" -align = 1 +horizontal_alignment = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 5 )] +[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 )] +[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="."] +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", -]] +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 131.0 offset_right = 34.0 @@ -63,21 +64,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 2 ) text = "Login" -[node name="unguessable_link_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -85,21 +82,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 2 ) text = "Unguessable Link" -[node name="guessable_link_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -107,16 +100,13 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 2 ) text = "Guessable Link" [connection signal="pressed" from="back" to="." method="_on_back_pressed"] 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 index b78f9e2..7e14acd 100644 --- a/tests/remote_config/remote_config.tscn +++ b/tests/remote_config/remote_config.tscn @@ -14,6 +14,7 @@ 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="."] diff --git a/tests/storage/storage.gd b/tests/storage/storage.gd index 64f6a60..636ef60 100644 --- a/tests/storage/storage.gd +++ b/tests/storage/storage.gd @@ -1,4 +1,4 @@ -extends Node2D +extends Control # Script used for testing the Storage functions of the plugin 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 fa4f0f9..db3607a 100644 --- a/tests/storage/storage.tscn +++ b/tests/storage/storage.tscn @@ -1,98 +1,88 @@ -[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="Texture2D" id=2] -[ext_resource path="res://assets/background.png" type="Texture2D" id=3] -[ext_resource path="res://fonts/PermanentMarker_30.tres" type="FontFile" 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="Texture2D" id=6] -[ext_resource path="res://assets/buttons/normal_button.gd" type="Script" id=7] -[ext_resource path="res://fonts/PermanentMarker.ttf" type="FontFile" id=8] -[ext_resource path="res://fonts/PermanentMarker_18.tres" type="FontFile" id=9] - - -[sub_resource type="FontFile" 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="."] -offset_right = 1024.0 -offset_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="."] +layout_mode = 0 offset_left = 376.0 offset_top = 12.0 offset_right = 557.0 offset_bottom = 56.0 -custom_fonts/font = ExtResource( 4 ) text = "Storage Tests" -align = 1 +horizontal_alignment = 1 uppercase = true -__meta__ = { -"_edit_use_anchors_": false -} -[node name="back" parent="." instance=ExtResource( 5 )] +[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 offset_top = 65.0 offset_right = 190.0 offset_bottom = 110.0 -texture_normal = ExtResource( 2 ) -texture_disabled = ExtResource( 6 ) -script = ExtResource( 7 ) -__meta__ = { -"_edit_use_anchors_": false -} +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"] +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="."] +layout_mode = 0 offset_left = 40.0 offset_top = 450.0 offset_right = 104.0 offset_bottom = 514.0 -__meta__ = { -"_edit_use_anchors_": false -} [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 -__meta__ = { -"_edit_use_anchors_": false -} -[node name="login_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[node name="login_check" type="CheckBox" parent="." groups=["tests"]] +layout_mode = 0 offset_left = 10.0 offset_top = 131.0 offset_right = 34.0 @@ -100,21 +90,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Login" -[node name="upload_image_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -122,21 +108,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Upload Image" -[node name="download_image_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -144,21 +126,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Download Image" -[node name="image_url_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -166,21 +144,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Image URL" -[node name="image_meta_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -188,21 +162,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Image Metadata" -[node name="image_delete_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -210,21 +180,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Delete Image" -[node name="upload_document_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -232,21 +198,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Upload Document" -[node name="document_meta_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -254,21 +216,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Document Metadata" -[node name="document_delete_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -276,21 +234,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Delete Document" -[node name="upload_string_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -298,21 +252,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Upload String" -[node name="string_add_meta_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -320,21 +270,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Add Metadata" -[node name="string_meta_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -342,21 +288,17 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "String Metadata" -[node name="string_delete_check" type="CheckBox" parent="." groups=[ -"tests", -]] +[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 @@ -364,16 +306,13 @@ 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"] +layout_mode = 0 offset_left = 26.0596 offset_top = -3.06488 offset_right = 76.0596 offset_bottom = 22.9351 -custom_fonts/font = ExtResource( 9 ) text = "Delete String" [connection signal="pressed" from="back" to="." method="_on_back_pressed"]