From a73193c860645a481c8cb7e3aa19746b258975dc Mon Sep 17 00:00:00 2001 From: Aaron Franke Date: Sat, 27 Aug 2022 14:19:25 -0500 Subject: [PATCH] Format GDScript files using Razoric's formatter + manual review --- addons/godot-firebase/auth/auth.gd | 826 ++++++------- addons/godot-firebase/auth/auth_provider.gd | 25 +- .../godot-firebase/auth/providers/facebook.gd | 35 +- .../godot-firebase/auth/providers/github.gd | 23 +- .../godot-firebase/auth/providers/google.gd | 19 +- .../godot-firebase/auth/providers/twitter.gd | 60 +- addons/godot-firebase/auth/user_data.gd | 68 +- addons/godot-firebase/database/database.gd | 74 +- .../godot-firebase/database/database_store.gd | 102 +- addons/godot-firebase/database/reference.gd | 363 +++--- addons/godot-firebase/database/resource.gd | 13 +- .../dynamiclinks/dynamiclinks.gd | 171 +-- addons/godot-firebase/firebase/firebase.gd | 184 +-- addons/godot-firebase/firestore/firestore.gd | 390 +++--- .../firestore/firestore_collection.gd | 130 +- .../firestore/firestore_document.gd | 266 +++-- .../firestore/firestore_query.gd | 284 ++--- .../firestore/firestore_task.gd | 546 ++++----- .../godot-firebase/functions/function_task.gd | 61 +- addons/godot-firebase/functions/functions.gd | 196 +-- addons/godot-firebase/plugin.gd | 6 +- addons/godot-firebase/storage/storage.gd | 650 +++++----- .../storage/storage_reference.gd | 199 +-- addons/godot-firebase/storage/storage_task.gd | 58 +- addons/http-sse-client/HTTPSSEClient.gd | 187 +-- .../http-sse-client/httpsseclient_plugin.gd | 6 +- .../class_doc_generator.gd | 2 + .../doc_exporter/doc_exporter.gd | 3 +- .../doc_item/argument_doc_item.gd | 4 +- .../doc_item/class_doc_item.gd | 7 +- .../doc_item/constant_doc_item.gd | 3 +- .../doc_item/doc_item.gd | 3 +- .../doc_item/method_doc_item.gd | 3 +- .../doc_item/property_doc_item.gd | 4 +- .../doc_item/signal_doc_item.gd | 4 +- addons/silicon.util.custom_docs/plugin.gd | 1064 ++++++++--------- test/TestUtils.gd | 7 +- test/firestore_test.gd | 34 +- test/storage_stress_test.gd | 78 +- test/unit/test_FirebaseDatabaseStore.gd | 257 ++-- test/unit/test_FirestoreDocument.gd | 119 +- 41 files changed, 3314 insertions(+), 3220 deletions(-) diff --git a/addons/godot-firebase/auth/auth.gd b/addons/godot-firebase/auth/auth.gd index 549d7de..4c284b0 100644 --- a/addons/godot-firebase/auth/auth.gd +++ b/addons/godot-firebase/auth/auth.gd @@ -6,8 +6,9 @@ tool class_name FirebaseAuth extends HTTPRequest -const _API_VERSION : String = "v1" -const _INAPP_PLUGIN : String = "GodotSvc" + +const _API_VERSION: String = "v1" +const _INAPP_PLUGIN: String = "GodotSvc" # Emitted for each Auth request issued. # `result_code` -> Either `1` if auth succeeded or `error_code` if unsuccessful auth request @@ -23,196 +24,194 @@ signal token_exchanged(successful) signal token_refresh_succeeded(auth_result) signal logged_out() -const RESPONSE_SIGNUP : String = "identitytoolkit#SignupNewUserResponse" -const RESPONSE_SIGNIN : String = "identitytoolkit#VerifyPasswordResponse" -const RESPONSE_ASSERTION : String = "identitytoolkit#VerifyAssertionResponse" -const RESPONSE_USERDATA : String = "identitytoolkit#GetAccountInfoResponse" -const RESPONSE_CUSTOM_TOKEN : String = "identitytoolkit#VerifyCustomTokenResponse" +const RESPONSE_SIGNUP: String = "identitytoolkit#SignupNewUserResponse" +const RESPONSE_SIGNIN: String = "identitytoolkit#VerifyPasswordResponse" +const RESPONSE_ASSERTION: String = "identitytoolkit#VerifyAssertionResponse" +const RESPONSE_USERDATA: String = "identitytoolkit#GetAccountInfoResponse" +const RESPONSE_CUSTOM_TOKEN: String = "identitytoolkit#VerifyCustomTokenResponse" -var _base_url : String = "" +var _base_url: String = "" var _refresh_request_base_url = "" -var _signup_request_url : String = "accounts:signUp?key=%s" -var _signin_with_oauth_request_url : String = "accounts:signInWithIdp?key=%s" -var _signin_request_url : String = "accounts:signInWithPassword?key=%s" -var _signin_custom_token_url : String = "accounts:signInWithCustomToken?key=%s" -var _userdata_request_url : String = "accounts:lookup?key=%s" -var _oobcode_request_url : String = "accounts:sendOobCode?key=%s" -var _delete_account_request_url : String = "accounts:delete?key=%s" -var _update_account_request_url : String = "accounts:update?key=%s" - -var _refresh_request_url : String = "/v1/token?key=%s" -var _google_auth_request_url : String = "https://accounts.google.com/o/oauth2/v2/auth?" - -var _config : Dictionary = {} -var auth : Dictionary = {} -var _needs_refresh : bool = false -var is_busy : bool = false -var has_child : bool = false - - -var tcp_server : TCP_Server = TCP_Server.new() -var tcp_timer : Timer = Timer.new() -var tcp_timeout : float = 0.5 - -var _headers : PoolStringArray = [ - "Content-Type: application/json", - "Accept: application/json", +var _signup_request_url: String = "accounts:signUp?key=%s" +var _signin_with_oauth_request_url: String = "accounts:signInWithIdp?key=%s" +var _signin_request_url: String = "accounts:signInWithPassword?key=%s" +var _signin_custom_token_url: String = "accounts:signInWithCustomToken?key=%s" +var _userdata_request_url: String = "accounts:lookup?key=%s" +var _oobcode_request_url: String = "accounts:sendOobCode?key=%s" +var _delete_account_request_url: String = "accounts:delete?key=%s" +var _update_account_request_url: String = "accounts:update?key=%s" + +var _refresh_request_url: String = "/v1/token?key=%s" +var _google_auth_request_url: String = "https://accounts.google.com/o/oauth2/v2/auth?" + +var _config: Dictionary = {} +var auth: Dictionary = {} +var _needs_refresh: bool = false +var is_busy: bool = false +var has_child: bool = false + +var tcp_server: TCP_Server = TCP_Server.new() +var tcp_timer: Timer = Timer.new() +var tcp_timeout: float = 0.5 + +var _headers: PoolStringArray = [ + "Content-Type: application/json", + "Accept: application/json", ] -var requesting : int = -1 +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 +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, +var _login_request_body: Dictionary = { + "email": "", + "password": "", + "returnSecureToken": true, } -var _oauth_login_request_body : Dictionary = { - "postBody":"", - "requestUri":"", - "returnIdpCredential":false, - "returnSecureToken":true +var _oauth_login_request_body: Dictionary = { + "postBody": "", + "requestUri": "", + "returnIdpCredential": false, + "returnSecureToken": true, } -var _anonymous_login_request_body : Dictionary = { - "returnSecureToken":true +var _anonymous_login_request_body: Dictionary = { + "returnSecureToken": true, } -var _refresh_request_body : Dictionary = { - "grant_type":"refresh_token", - "refresh_token":"", +var _refresh_request_body: Dictionary = { + "grant_type": "refresh_token", + "refresh_token": "", } -var _custom_token_body : Dictionary = { - "token":"", - "returnSecureToken":true +var _custom_token_body: Dictionary = { + "token": "", + "returnSecureToken": true, } -var _password_reset_body : Dictionary = { - "requestType":"password_reset", - "email":"", +var _password_reset_body: Dictionary = { + "requestType": "password_reset", + "email": "", } - -var _change_email_body : Dictionary = { - "idToken":"", - "email":"", - "returnSecureToken": true, +var _change_email_body: Dictionary = { + "idToken": "", + "email": "", + "returnSecureToken": true, } - -var _change_password_body : Dictionary = { - "idToken":"", - "password":"", - "returnSecureToken": true, +var _change_password_body: Dictionary = { + "idToken": "", + "password": "", + "returnSecureToken": true, } - -var _account_verification_body : Dictionary = { - "requestType":"verify_email", - "idToken":"", +var _account_verification_body: Dictionary = { + "requestType": "verify_email", + "idToken": "", } - -var _update_profile_body : Dictionary = { - "idToken":"", - "displayName":"", - "photoUrl":"", - "deleteAttribute":"", - "returnSecureToken":true +var _update_profile_body: Dictionary = { + "idToken": "", + "displayName": "", + "photoUrl": "", + "deleteAttribute": "", + "returnSecureToken": true, } -var _local_port : int = 8060 -var _local_uri : String = "http://localhost:%s/"%_local_port -var _local_provider : AuthProvider = AuthProvider.new() +var _local_port: int = 8060 +var _local_uri: String = "http://localhost:%s/" % _local_port +var _local_provider: AuthProvider = AuthProvider.new() + func _ready() -> void: - tcp_timer.wait_time = tcp_timeout - tcp_timer.connect("timeout", self, "_tcp_stream_timer") - - if OS.get_name() == "HTML5": - _local_uri += "tmp_js_export.html" + tcp_timer.wait_time = tcp_timeout + tcp_timer.connect("timeout", self, "_tcp_stream_timer") + + if OS.get_name() == "HTML5": + _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 - - connect("request_completed", self, "_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}) +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 + + connect("request_completed", self, "_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}) # Function is used to check if the auth script is ready to process a request. Returns true if it is not currently processing # If false it will print an error func _is_ready() -> bool: - if is_busy: - Firebase._printerr("Firebase Auth is currently busy and cannot process this request") - return false - else: - return true + if is_busy: + Firebase._printerr("Firebase Auth is currently busy and cannot process this request") + return false + else: + return true + # Function cleans the URI and replaces spaces with %20 # As of right now we only replace spaces # We may need to decide to use the percent_encode() String function func _clean_url(_url): - _url = _url.replace(' ','%20') - return _url + _url = _url.replace(" ", "%20") + return _url + # Synchronous call to check if any user is already logged in. func is_logged_in() -> bool: - return auth != null and auth.has("idtoken") + return auth != null and auth.has("idtoken") # Called with Firebase.Auth.signup_with_email_and_password(email, password) # You must pass in the email and password to this function for it to work correctly -func signup_with_email_and_password(email : String, password : String) -> void: - if _is_ready(): - is_busy = true - _login_request_body.email = email - _login_request_body.password = password - auth_request_type = Auth_Type.SIGNUP_EP - request(_base_url + _signup_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_login_request_body)) +func signup_with_email_and_password(email: String, password: String) -> void: + if _is_ready(): + is_busy = true + _login_request_body.email = email + _login_request_body.password = password + auth_request_type = Auth_Type.SIGNUP_EP + request(_base_url + _signup_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_login_request_body)) # Called with Firebase.Auth.anonymous_login() @@ -220,395 +219,398 @@ func signup_with_email_and_password(email : String, password : String) -> void: # The response contains the Firebase ID token and refresh token associated with the anonymous user. # The 'mail' field will be empty since no email is linked to an anonymous user func login_anonymous() -> void: - if _is_ready(): - is_busy = true - auth_request_type = Auth_Type.LOGIN_ANON - request(_base_url + _signup_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_anonymous_login_request_body)) + if _is_ready(): + is_busy = true + auth_request_type = Auth_Type.LOGIN_ANON + request(_base_url + _signup_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_anonymous_login_request_body)) # Called with Firebase.Auth.login_with_email_and_password(email, password) # You must pass in the email and password to this function for it to work correctly # If the login fails it will return an error code through the function _on_FirebaseAuth_request_completed -func login_with_email_and_password(email : String, password : String) -> void: - if _is_ready(): - is_busy = true - _login_request_body.email = email - _login_request_body.password = password - auth_request_type = Auth_Type.LOGIN_EP - request(_base_url + _signin_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_login_request_body)) +func login_with_email_and_password(email: String, password: String) -> void: + if _is_ready(): + is_busy = true + _login_request_body.email = email + _login_request_body.password = password + auth_request_type = Auth_Type.LOGIN_EP + request(_base_url + _signin_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_login_request_body)) + # Login with a custom valid token # The token needs to be generated using an external service/function -func login_with_custom_token(token : String) -> void: - if _is_ready(): - is_busy = true - _custom_token_body.token = token - auth_request_type = Auth_Type.LOGIN_CT - request(_base_url + _signin_custom_token_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_custom_token_body)) +func login_with_custom_token(token: String) -> void: + if _is_ready(): + is_busy = true + _custom_token_body.token = token + auth_request_type = Auth_Type.LOGIN_CT + request(_base_url + _signin_custom_token_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_custom_token_body)) # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** the generated token will be automatically captured and a login request will be made if the token is correct -func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port : int = _local_port): - get_auth_with_redirect(provider) - yield(get_tree().create_timer(0.5),"timeout") - if has_child == false: - add_child(tcp_timer) - has_child = true - tcp_timer.start() - tcp_server.listen(port, "*") +func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port: int = _local_port): + get_auth_with_redirect(provider) + yield(get_tree().create_timer(0.5),"timeout") + if has_child == false: + add_child(tcp_timer) + has_child = true + tcp_timer.start() + tcp_server.listen(port, "*") func get_auth_with_redirect(provider: AuthProvider) -> void: - var url_endpoint: String = provider.redirect_uri - for key in provider.params.keys(): - url_endpoint+=key+"="+provider.params[key]+"&" - url_endpoint += provider.params.redirect_type+"="+_local_uri - url_endpoint = _clean_url(url_endpoint) - if OS.get_name() == "HTML5" and OS.has_feature("JavaScript"): - JavaScript.eval('window.location.replace("' + url_endpoint + '")') - elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": - #in app for ios if the iOS plugin exists - set_local_provider(provider) - Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint) - else: - set_local_provider(provider) - OS.shell_open(url_endpoint) - print(url_endpoint) + var url_endpoint: String = provider.redirect_uri + for key in provider.params.keys(): + url_endpoint += key + "=" + provider.params[key] + "&" + url_endpoint += provider.params.redirect_type + "=" + _local_uri + url_endpoint = _clean_url(url_endpoint) + if OS.get_name() == "HTML5" and OS.has_feature("JavaScript"): + JavaScript.eval('window.location.replace("' + url_endpoint + '")') + elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": + # in app for ios if the iOS plugin exists + set_local_provider(provider) + Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint) + else: + set_local_provider(provider) + OS.shell_open(url_endpoint) + print(url_endpoint) # Login with Google oAuth2. # A token is automatically obtained using an authorization code using @get_google_auth() # @provider_id and @request_uri can be changed func login_with_oauth(_token: String, provider: AuthProvider) -> void: - var token : String = _token.percent_decode() - print(token) - var is_successful: bool = true - if provider.should_exchange: - exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) - is_successful = yield(self, "token_exchanged") - token = auth.accesstoken - if is_successful and _is_ready(): - is_busy = true - _oauth_login_request_body.postBody = "access_token="+token+"&providerId="+provider.provider_id - _oauth_login_request_body.requestUri = _local_uri - requesting = Requests.LOGIN_WITH_OAUTH - auth_request_type = Auth_Type.LOGIN_OAUTH - request(_base_url + _signin_with_oauth_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_oauth_login_request_body)) - + 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)) # Exchange the authorization oAuth2 code obtained from browser with a proper access id_token -func exchange_token(code : String, redirect_uri : String, request_url: String, _client_id: String, _client_secret: String) -> void: - if _is_ready(): - is_busy = true - var exchange_token_body : Dictionary = { - code = code, - redirect_uri = redirect_uri, - client_id = _client_id, - client_secret = _client_secret, - grant_type = "authorization_code", - } - requesting = Requests.EXCHANGE_TOKEN - request(request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(exchange_token_body)) - +func exchange_token(code: String, redirect_uri: String, request_url: String, _client_id: String, _client_secret: String) -> void: + if _is_ready(): + is_busy = true + var exchange_token_body: Dictionary = { + code = code, + redirect_uri = redirect_uri, + client_id = _client_id, + client_secret = _client_secret, + grant_type = "authorization_code", + } + requesting = Requests.EXCHANGE_TOKEN + request(request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(exchange_token_body)) # Open a web page in browser redirecting to Google oAuth2 page for the current project # Once given user's authorization, a token will be generated. # NOTE** with this method, the authorization process will be copy-pasted func get_google_auth_manual(provider: AuthProvider = _local_provider) -> void: - provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" - get_auth_with_redirect(provider) + provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" + get_auth_with_redirect(provider) # A timer used to listen through TCP on the redirect uri of the request func _tcp_stream_timer() -> void: - var peer : StreamPeer = tcp_server.take_connection() - if peer != null: - var raw_result : String = peer.get_utf8_string(400) - if raw_result != "" and raw_result.begins_with("GET"): - tcp_timer.stop() - remove_child(tcp_timer) - has_child = false - var token : String = "" - for value in raw_result.split(" ")[1].lstrip("/?").split("&"): - var splitted: PoolStringArray = value.split("=") - if _local_provider.params.response_type in splitted[0]: - token = splitted[1] - break - if token == "": - emit_signal("login_failed") - peer.disconnect_from_host() - tcp_server.stop() - var data : PoolByteArray = '

🔥 You can close this window now. 🔥

'.to_ascii() - peer.put_data(("HTTP/1.1 200 OK\n").to_ascii()) - peer.put_data(("Server: Godot Firebase SDK\n").to_ascii()) - peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii()) - peer.put_data("Connection: close\n".to_ascii()) - peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii()) - peer.put_data(data) - login_with_oauth(token, _local_provider) - yield(self, "login_succeeded") - peer.disconnect_from_host() - tcp_server.stop() - - + var peer : StreamPeer = tcp_server.take_connection() + if peer != null: + var raw_result: String = peer.get_utf8_string(400) + if raw_result != "" and raw_result.begins_with("GET"): + tcp_timer.stop() + remove_child(tcp_timer) + has_child = false + var token: String = "" + for value in raw_result.split(" ")[1].lstrip("/?").split("&"): + var splitted: PoolStringArray = value.split("=") + if _local_provider.params.response_type in splitted[0]: + token = splitted[1] + break + if token == "": + emit_signal("login_failed") + peer.disconnect_from_host() + tcp_server.stop() + var data : PoolByteArray = '

🔥 You can close this window now. 🔥

'.to_ascii() + peer.put_data(("HTTP/1.1 200 OK\n").to_ascii()) + peer.put_data(("Server: Godot Firebase SDK\n").to_ascii()) + peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii()) + peer.put_data("Connection: close\n".to_ascii()) + peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii()) + peer.put_data(data) + login_with_oauth(token, _local_provider) + yield(self, "login_succeeded") + peer.disconnect_from_host() + tcp_server.stop() # Function used to logout of the system, this will also remove the local encrypted auth file if there is one func logout() -> void: - auth = {} - remove_auth() - emit_signal("logged_out") + auth = {} + remove_auth() + emit_signal("logged_out") # Function is called when requesting a manual token refresh func manual_token_refresh(auth_data): - auth = auth_data - var refresh_token = null - auth = get_clean_keys(auth) - if auth.has("refreshtoken"): - refresh_token = auth.refreshtoken - elif auth.has("refresh_token"): - refresh_token = auth.refresh_token - _needs_refresh = true - _refresh_request_body.refresh_token = refresh_token - request(_refresh_request_base_url + _refresh_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_refresh_request_body)) + auth = auth_data + var refresh_token = null + auth = get_clean_keys(auth) + if auth.has("refreshtoken"): + refresh_token = auth.refreshtoken + elif auth.has("refresh_token"): + refresh_token = auth.refresh_token + _needs_refresh = true + _refresh_request_body.refresh_token = refresh_token + request(_refresh_request_base_url + _refresh_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_refresh_request_body)) # This function is called whenever there is an authentication request to Firebase # On an error, this function with emit the signal 'login_failed' and print the error to the console -func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - print_debug(JSON.parse(body.get_string_from_utf8()).result) - is_busy = false - var res - if response_code == 0: - # Mocked error results to trigger the correct signal. - # Can occur if there is no internet connection, or the service is down, - # in which case there is no json_body (and thus parsing would fail). - res = {"error": { - "code": "Connection error", - "message": "Error connecting to auth service"}} - else: - var bod = body.get_string_from_utf8() - var json_result = JSON.parse(bod) - if json_result.error != OK: - Firebase._printerr("Error while parsing auth body json") - emit_signal("auth_request", ERR_PARSE_ERROR, "Error while parsing auth body json") - return - res = json_result.result - - if response_code == HTTPClient.RESPONSE_OK: - if not res.has("kind"): - auth = get_clean_keys(res) - match requesting: - Requests.EXCHANGE_TOKEN: - emit_signal("token_exchanged", true) - begin_refresh_countdown() - # Refresh token countdown - emit_signal("auth_request", 1, auth) - else: - match res.kind: - RESPONSE_SIGNUP: - auth = get_clean_keys(res) - emit_signal("signup_succeeded", auth) - begin_refresh_countdown() - RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: - auth = get_clean_keys(res) - emit_signal("login_succeeded", auth) - begin_refresh_countdown() - RESPONSE_USERDATA: - var userdata = FirebaseUserData.new(res.users[0]) - emit_signal("userdata_received", userdata) - emit_signal("auth_request", 1, auth) - else: - # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD - if requesting == Requests.EXCHANGE_TOKEN: - emit_signal("token_exchanged", false) - emit_signal("login_failed", res.error, res.error_description) - emit_signal("auth_request", res.error, res.error_description) - else: - var sig = "signup_failed" if auth_request_type == Auth_Type.SIGNUP_EP else "login_failed" - emit_signal(sig, res.error.code, res.error.message) - emit_signal("auth_request", res.error.code, res.error.message) - requesting = Requests.NONE - auth_request_type = Auth_Type.NONE +func _on_FirebaseAuth_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray) -> void: + print_debug(JSON.parse(body.get_string_from_utf8()).result) + is_busy = false + var res + if response_code == 0: + # Mocked error results to trigger the correct signal. + # Can occur if there is no internet connection, or the service is down, + # in which case there is no json_body (and thus parsing would fail). + res = {"error": { + "code": "Connection error", + "message": "Error connecting to auth service"}} + else: + var bod = body.get_string_from_utf8() + var json_result = JSON.parse(bod) + if json_result.error != OK: + Firebase._printerr("Error while parsing auth body json") + emit_signal("auth_request", ERR_PARSE_ERROR, "Error while parsing auth body json") + return + res = json_result.result + + if response_code == HTTPClient.RESPONSE_OK: + if not res.has("kind"): + auth = get_clean_keys(res) + match requesting: + Requests.EXCHANGE_TOKEN: + emit_signal("token_exchanged", true) + begin_refresh_countdown() + # Refresh token countdown + emit_signal("auth_request", 1, auth) + else: + match res.kind: + RESPONSE_SIGNUP: + auth = get_clean_keys(res) + emit_signal("signup_succeeded", auth) + begin_refresh_countdown() + RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: + auth = get_clean_keys(res) + emit_signal("login_succeeded", auth) + begin_refresh_countdown() + RESPONSE_USERDATA: + var userdata = FirebaseUserData.new(res.users[0]) + emit_signal("userdata_received", userdata) + emit_signal("auth_request", 1, auth) + else: + # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD + if requesting == Requests.EXCHANGE_TOKEN: + emit_signal("token_exchanged", false) + emit_signal("login_failed", res.error, res.error_description) + emit_signal("auth_request", res.error, res.error_description) + else: + var sig = "signup_failed" if auth_request_type == Auth_Type.SIGNUP_EP else "login_failed" + emit_signal(sig, res.error.code, res.error.message) + emit_signal("auth_request", res.error.code, res.error.message) + requesting = Requests.NONE + auth_request_type = Auth_Type.NONE # Function used to save the auth data provided by Firebase into an encrypted file # Note this does not work in HTML5 or UWP -func save_auth(auth : Dictionary) -> void: - var encrypted_file = File.new() - var err = encrypted_file.open_encrypted_with_pass("user://user.auth", File.WRITE, _config.apiKey) - if err != OK: - Firebase._printerr("Error Opening File. Error Code: " + String(err)) - else: - encrypted_file.store_line(to_json(auth)) - encrypted_file.close() +func save_auth(auth: Dictionary) -> void: + var encrypted_file = File.new() + var err = encrypted_file.open_encrypted_with_pass("user://user.auth", File.WRITE, _config.apiKey) + if err != OK: + Firebase._printerr("Error Opening File. Error Code: " + String(err)) + else: + encrypted_file.store_line(to_json(auth)) + encrypted_file.close() # Function used to load the auth data file that has been stored locally # Note this does not work in HTML5 or UWP func load_auth() -> void: - var encrypted_file = File.new() - var err = encrypted_file.open_encrypted_with_pass("user://user.auth", File.READ, _config.apiKey) - if err != OK: - Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(err)) - emit_signal("auth_request", err, "Error Opening Firebase Auth File.") - else: - var encrypted_file_data = parse_json(encrypted_file.get_line()) - manual_token_refresh(encrypted_file_data) + var encrypted_file = File.new() + var err = encrypted_file.open_encrypted_with_pass("user://user.auth", File.READ, _config.apiKey) + if err != OK: + Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(err)) + emit_signal("auth_request", err, "Error Opening Firebase Auth File.") + else: + var encrypted_file_data = parse_json(encrypted_file.get_line()) + manual_token_refresh(encrypted_file_data) # Function used to remove the local encrypted auth file func remove_auth() -> void: - var dir = Directory.new() - if (dir.file_exists("user://user.auth")): - dir.remove("user://user.auth") - else: - Firebase._printerr("No encrypted auth file exists") + var dir = Directory.new() + if dir.file_exists("user://user.auth"): + dir.remove("user://user.auth") + else: + Firebase._printerr("No encrypted auth file exists") # Function to check if there is an encrypted auth data file # If there is, the game will load it and refresh the token func check_auth_file() -> void: - var dir = Directory.new() - if (dir.file_exists("user://user.auth")): - # Will ensure "auth_request" emitted - load_auth() - else: - Firebase._printerr("Encrypted Firebase Auth file does not exist") - emit_signal("auth_request", ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") + var dir = Directory.new() + if dir.file_exists("user://user.auth"): + # Will ensure "auth_request" emitted + load_auth() + else: + Firebase._printerr("Encrypted Firebase Auth file does not exist") + emit_signal("auth_request", ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") # Function used to change the email account for the currently logged in user -func change_user_email(email : String) -> void: - if _is_ready(): - is_busy = true - _change_email_body.email = email - _change_email_body.idToken = auth.idtoken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_change_email_body)) +func change_user_email(email: String) -> void: + if _is_ready(): + is_busy = true + _change_email_body.email = email + _change_email_body.idToken = auth.idtoken + request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_change_email_body)) # Function used to change the password for the currently logged in user -func change_user_password(password : String) -> void: - if _is_ready(): - is_busy = true - _change_password_body.password = password - _change_password_body.idToken = auth.idtoken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_change_password_body)) +func change_user_password(password: String) -> void: + if _is_ready(): + is_busy = true + _change_password_body.password = password + _change_password_body.idToken = auth.idtoken + request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_change_password_body)) # User Profile handlers -func update_account(idToken : String, displayName : String, photoUrl : String, deleteAttribute : PoolStringArray, returnSecureToken : bool) -> void: - if _is_ready(): - is_busy = true - _update_profile_body.idToken = idToken - _update_profile_body.displayName = displayName - _update_profile_body.photoUrl = photoUrl - _update_profile_body.deleteAttribute = deleteAttribute - _update_profile_body.returnSecureToken = returnSecureToken - request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_update_profile_body)) +func update_account(idToken: String, displayName: String, photoUrl: String, deleteAttribute: PoolStringArray, returnSecureToken: bool) -> void: + if _is_ready(): + is_busy = true + _update_profile_body.idToken = idToken + _update_profile_body.displayName = displayName + _update_profile_body.photoUrl = photoUrl + _update_profile_body.deleteAttribute = deleteAttribute + _update_profile_body.returnSecureToken = returnSecureToken + request(_base_url + _update_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_update_profile_body)) # Function to send a account verification email func send_account_verification_email() -> void: - if _is_ready(): - is_busy = true - _account_verification_body.idToken = auth.idtoken - request(_base_url + _oobcode_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_account_verification_body)) + if _is_ready(): + is_busy = true + _account_verification_body.idToken = auth.idtoken + request(_base_url + _oobcode_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_account_verification_body)) # Function used to reset the password for a user who has forgotten in. # This will send the users account an email with a password reset link -func send_password_reset_email(email : String) -> void: - if _is_ready(): - is_busy = true - _password_reset_body.email = email - request(_base_url + _oobcode_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_password_reset_body)) +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)) # Function called to get all func get_user_data() -> void: - if _is_ready(): - is_busy = true - if not is_logged_in(): - print_debug("Not logged in") - is_busy = false - return + if _is_ready(): + is_busy = true + if not is_logged_in(): + print_debug("Not logged in") + is_busy = false + return - request(_base_url + _userdata_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print({"idToken":auth.idtoken})) + request(_base_url + _userdata_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print({"idToken": auth.idtoken})) # Function used to delete the account of the currently authenticated user func delete_user_account() -> void: - if _is_ready(): - is_busy = true - request(_base_url + _delete_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print({"idToken":auth.idtoken})) + if _is_ready(): + is_busy = true + request(_base_url + _delete_account_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print({"idToken": auth.idtoken})) # Function is called when a new token is issued to a user. The function will yield until the token has expired, and then request a new one. func begin_refresh_countdown() -> void: - var refresh_token = null - var expires_in = 1000 - auth = get_clean_keys(auth) - if auth.has("refreshtoken"): - refresh_token = auth.refreshtoken - expires_in = auth.expiresin - elif auth.has("refresh_token"): - refresh_token = auth.refresh_token - expires_in = auth.expires_in - if auth.has("userid"): - auth["localid"] = auth.userid - _needs_refresh = true - emit_signal("token_refresh_succeeded", auth) - yield(get_tree().create_timer(float(expires_in)), "timeout") - _refresh_request_body.refresh_token = refresh_token - request(_refresh_request_base_url + _refresh_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_refresh_request_body)) + var refresh_token = null + var expires_in = 1000 + auth = get_clean_keys(auth) + if auth.has("refreshtoken"): + refresh_token = auth.refreshtoken + expires_in = auth.expiresin + elif auth.has("refresh_token"): + refresh_token = auth.refresh_token + expires_in = auth.expires_in + if auth.has("userid"): + auth["localid"] = auth.userid + _needs_refresh = true + emit_signal("token_refresh_succeeded", auth) + yield(get_tree().create_timer(float(expires_in)), "timeout") + _refresh_request_body.refresh_token = refresh_token + request(_refresh_request_base_url + _refresh_request_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_refresh_request_body)) func get_token_from_url(provider: AuthProvider): - var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" - if OS.has_feature('JavaScript'): - var token = JavaScript.eval(""" - var url_string = window.location.href.replaceAll('?#', '?'); - var url = new URL(url_string); - url.searchParams.get('"""+token_type+"""'); - """) - JavaScript.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") - return token - return null + var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" + if OS.has_feature('JavaScript'): + var token = JavaScript.eval(""" + var url_string = window.location.href.replaceAll('?#', '?'); + var url = new URL(url_string); + url.searchParams.get('"""+token_type+"""'); + """) + JavaScript.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") + return token + return null + + +func set_redirect_uri(redirect_uri: String) -> void: + self._local_uri = redirect_uri -func set_redirect_uri(redirect_uri : String) -> void: - self._local_uri = redirect_uri +func set_local_provider(provider: AuthProvider) -> void: + self._local_provider = provider -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 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 +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 + # -------------------- # PROVIDERS # -------------------- func get_GoogleProvider() -> GoogleProvider: - return GoogleProvider.new(_config.clientId, _config.clientSecret) + return GoogleProvider.new(_config.clientId, _config.clientSecret) + func get_FacebookProvider() -> FacebookProvider: - return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret) + return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret) + func get_GitHubProvider() -> GitHubProvider: - return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret) + return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret) + func get_TwitterProvider() -> TwitterProvider: - return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret) + return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret) diff --git a/addons/godot-firebase/auth/auth_provider.gd b/addons/godot-firebase/auth/auth_provider.gd index ec0742d..802b84a 100644 --- a/addons/godot-firebase/auth/auth_provider.gd +++ b/addons/godot-firebase/auth/auth_provider.gd @@ -2,31 +2,36 @@ tool class_name AuthProvider extends Reference + var redirect_uri: String = "" var access_token_uri: String = "" var provider_id: String = "" var params: Dictionary = { - client_id = "", - scope = "", - response_type = "", - state = "", - redirect_type = "redirect_uri", + client_id = "", + scope = "", + response_type = "", + state = "", + redirect_type = "redirect_uri", } var client_secret: String = "" var should_exchange: bool = false func set_client_id(client_id: String) -> void: - self.params.client_id = client_id + self.params.client_id = client_id + func set_client_secret(client_secret: String) -> void: - self.client_secret = client_secret + self.client_secret = client_secret + func get_client_id() -> String: - return self.params.client_id + return self.params.client_id + func get_client_secret() -> String: - return self.client_secret + return self.client_secret + func get_oauth_params() -> String: - return "" + return "" diff --git a/addons/godot-firebase/auth/providers/facebook.gd b/addons/godot-firebase/auth/providers/facebook.gd index 32c1290..fcabb5f 100644 --- a/addons/godot-firebase/auth/providers/facebook.gd +++ b/addons/godot-firebase/auth/providers/facebook.gd @@ -1,21 +1,20 @@ -class_name FacebookProvider +class_name FacebookProvider extends AuthProvider + func _init(client_id: String, client_secret: String) -> void: - randomize() - set_client_id(client_id) - set_client_secret(client_secret) - - self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?" - self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token" - self.provider_id = "facebook.com" - self.params.scope = "public_profile" - self.params.state = str(rand_range(0, 1)) - if OS.get_name() == "HTML5": - self.should_exchange = false - self.params.response_type = "token" - else: - self.should_exchange = true - self.params.response_type = "code" - - + randomize() + set_client_id(client_id) + set_client_secret(client_secret) + + self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?" + self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token" + self.provider_id = "facebook.com" + self.params.scope = "public_profile" + self.params.state = str(rand_range(0, 1)) + if OS.get_name() == "HTML5": + self.should_exchange = false + self.params.response_type = "token" + else: + self.should_exchange = true + self.params.response_type = "code" diff --git a/addons/godot-firebase/auth/providers/github.gd b/addons/godot-firebase/auth/providers/github.gd index 1716258..0a64884 100644 --- a/addons/godot-firebase/auth/providers/github.gd +++ b/addons/godot-firebase/auth/providers/github.gd @@ -1,14 +1,15 @@ -class_name GitHubProvider +class_name GitHubProvider extends AuthProvider + func _init(client_id: String, client_secret: String) -> void: - randomize() - set_client_id(client_id) - set_client_secret(client_secret) - self.should_exchange = true - self.redirect_uri = "https://github.com/login/oauth/authorize?" - 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.response_type = "code" + randomize() + set_client_id(client_id) + set_client_secret(client_secret) + self.should_exchange = true + self.redirect_uri = "https://github.com/login/oauth/authorize?" + 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.response_type = "code" diff --git a/addons/godot-firebase/auth/providers/google.gd b/addons/godot-firebase/auth/providers/google.gd index 48ad38f..3857b78 100644 --- a/addons/godot-firebase/auth/providers/google.gd +++ b/addons/godot-firebase/auth/providers/google.gd @@ -1,13 +1,14 @@ class_name GoogleProvider extends AuthProvider + func _init(client_id: String, client_secret: String) -> void: - set_client_id(client_id) - set_client_secret(client_secret) - self.should_exchange = true - self.redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth?" - self.access_token_uri = "https://oauth2.googleapis.com/token" - self.provider_id = "google.com" - self.params.response_type = "code" - self.params.scope = "email openid profile" - self.params.response_type = "code" + set_client_id(client_id) + set_client_secret(client_secret) + self.should_exchange = true + self.redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth?" + self.access_token_uri = "https://oauth2.googleapis.com/token" + self.provider_id = "google.com" + self.params.response_type = "code" + self.params.scope = "email openid profile" + self.params.response_type = "code" diff --git a/addons/godot-firebase/auth/providers/twitter.gd b/addons/godot-firebase/auth/providers/twitter.gd index b4c37b7..0c3582e 100644 --- a/addons/godot-firebase/auth/providers/twitter.gd +++ b/addons/godot-firebase/auth/providers/twitter.gd @@ -1,39 +1,41 @@ -class_name TwitterProvider +class_name TwitterProvider extends AuthProvider + var request_token_endpoint: String = "https://api.twitter.com/oauth/access_token?oauth_callback=" var oauth_header: Dictionary = { - oauth_callback="", - oauth_consumer_key="", - oauth_nonce="", - oauth_signature="", - oauth_signature_method="HMAC-SHA1", - oauth_timestamp="", - oauth_version="1.0" + oauth_callback = "", + oauth_consumer_key = "", + oauth_nonce = "", + oauth_signature = "", + oauth_signature_method = "HMAC-SHA1", + oauth_timestamp = "", + oauth_version = "1.0" } + func _init(client_id: String, client_secret: String) -> void: - 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.should_exchange = true - self.redirect_uri = "https://twitter.com/i/oauth2/authorize?" - self.access_token_uri = "https://api.twitter.com/2/oauth2/token" - self.provider_id = "twitter.com" - self.params.redirect_type = "redirect_uri" - self.params.response_type = "code" - self.params.scope = "users.read" - self.params.state = str(rand_range(0, 1)) + 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.should_exchange = true + self.redirect_uri = "https://twitter.com/i/oauth2/authorize?" + self.access_token_uri = "https://api.twitter.com/2/oauth2/token" + self.provider_id = "twitter.com" + self.params.redirect_type = "redirect_uri" + self.params.response_type = "code" + self.params.scope = "users.read" + self.params.state = str(rand_range(0, 1)) + func get_oauth_params() -> String: - var params: PoolStringArray = [] - for key in self.oauth.keys(): - params.append(key+"="+self.oauth.get(key)) - return params.join("&") + var params: PoolStringArray = [] + for key in self.oauth.keys(): + params.append(key + "=" + self.oauth.get(key)) + return params.join("&") diff --git a/addons/godot-firebase/auth/user_data.gd b/addons/godot-firebase/auth/user_data.gd index a5ddb85..11cee00 100644 --- a/addons/godot-firebase/auth/user_data.gd +++ b/addons/godot-firebase/auth/user_data.gd @@ -6,39 +6,43 @@ tool class_name FirebaseUserData extends Reference -var local_id : String = "" # The uid of the current user. -var email : String = "" -var email_verified := false # Whether or not the account's email has been verified. -var password_updated_at : float = 0 # The timestamp, in milliseconds, that the account password was last changed. -var last_login_at : float = 0 # The timestamp, in milliseconds, that the account last logged in at. -var created_at : float = 0 # The timestamp, in milliseconds, that the account was created at. -var provider_user_info : Array = [] - -var provider_id : String = "" -var display_name : String = "" -var photo_url : String = "" - -func _init(p_userdata : Dictionary) -> void: - local_id = p_userdata.get("localId", "") - email = p_userdata.get("email", "") - email_verified = p_userdata.get("emailVerified", false) - last_login_at = float(p_userdata.get("lastLoginAt", 0)) - created_at = float(p_userdata.get("createdAt", 0)) - 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(): - provider_id = provider_user_info[0].get("providerId", "") - photo_url = provider_user_info[0].get("photoUrl", "") - display_name = provider_user_info[0].get("displayName", "") + +var local_id: String = "" # The uid of the current user. +var email: String = "" +var email_verified := false # Whether or not the account's email has been verified. +var password_updated_at: float = 0 # The timestamp, in milliseconds, that the account password was last changed. +var last_login_at: float = 0 # The timestamp, in milliseconds, that the account last logged in at. +var created_at: float = 0 # The timestamp, in milliseconds, that the account was created at. +var provider_user_info: Array = [] + +var provider_id: String = "" +var display_name: String = "" +var photo_url: String = "" + + +func _init(p_userdata: Dictionary) -> void: + local_id = p_userdata.get("localId", "") + email = p_userdata.get("email", "") + email_verified = p_userdata.get("emailVerified", false) + last_login_at = float(p_userdata.get("lastLoginAt", 0)) + created_at = float(p_userdata.get("createdAt", 0)) + 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(): + provider_id = provider_user_info[0].get("providerId", "") + photo_url = provider_user_info[0].get("photoUrl", "") + display_name = provider_user_info[0].get("displayName", "") + func as_text() -> String: - return _to_string() + return _to_string() + func _to_string() -> String: - var txt = "local_id : %s\n" % local_id - txt += "email : %s\n" % email - txt += "last_login_at : %d\n" % last_login_at - txt += "provider_id : %s\n" % provider_id - txt += "display name : %s\n" % display_name - return txt + var txt = "local_id : %s\n" % local_id + txt += "email : %s\n" % email + txt += "last_login_at : %d\n" % last_login_at + txt += "provider_id : %s\n" % provider_id + txt += "display name : %s\n" % display_name + return txt diff --git a/addons/godot-firebase/database/database.gd b/addons/godot-firebase/database/database.gd index 1d51b91..64b435f 100644 --- a/addons/godot-firebase/database/database.gd +++ b/addons/godot-firebase/database/database.gd @@ -6,49 +6,53 @@ tool class_name FirebaseDatabase extends Node -var _base_url : String = "" -var _config : Dictionary = {} +var _base_url: String = "" -var _auth : Dictionary = {} +var _config: Dictionary = {} -func _set_config(config_json : Dictionary) -> void: - _config = config_json - _check_emulating() +var _auth: Dictionary = {} -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" +func _set_config(config_json: Dictionary) -> void: + _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" -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_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_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: - var firebase_reference : FirebaseDatabaseReference = FirebaseDatabaseReference.new() - var pusher : HTTPRequest = HTTPRequest.new() - var listener : Node = Node.new() - listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) - var store : FirebaseDatabaseStore = FirebaseDatabaseStore.new() - firebase_reference.set_db_path(path, filter) - firebase_reference.set_auth_and_config(_auth, _config) - firebase_reference.set_pusher(pusher) - firebase_reference.set_listener(listener) - firebase_reference.set_store(store) - add_child(firebase_reference) - return firebase_reference + _auth = {} + + +func get_database_reference(path: String, filter: Dictionary = {}) -> FirebaseDatabaseReference: + var firebase_reference: FirebaseDatabaseReference = FirebaseDatabaseReference.new() + var pusher: HTTPRequest = HTTPRequest.new() + var listener: Node = Node.new() + listener.set_script(load("res://addons/http-sse-client/HTTPSSEClient.gd")) + var store: FirebaseDatabaseStore = FirebaseDatabaseStore.new() + firebase_reference.set_db_path(path, filter) + firebase_reference.set_auth_and_config(_auth, _config) + firebase_reference.set_pusher(pusher) + firebase_reference.set_listener(listener) + firebase_reference.set_store(store) + add_child(firebase_reference) + return firebase_reference diff --git a/addons/godot-firebase/database/database_store.gd b/addons/godot-firebase/database/database_store.gd index 07b6753..ff49e8c 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 = PoolStringArray([_ROOT]) + # + # Traverse the path. + # + var dict = _data + var keys = PoolStringArray([_ROOT]) - keys.append_array(path.split(_DELIMITER, false)) + keys.append_array(path.split(_DELIMITER, false)) - var final_key_idx = (keys.size() - 1) - var final_key = (keys[final_key_idx]) + var final_key_idx = (keys.size() - 1) + var final_key = (keys[final_key_idx]) - keys.remove(final_key_idx) + keys.remove(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/reference.gd b/addons/godot-firebase/database/reference.gd index b4ae5c8..f333ab5 100644 --- a/addons/godot-firebase/database/reference.gd +++ b/addons/godot-firebase/database/reference.gd @@ -6,6 +6,7 @@ tool class_name FirebaseDatabaseReference extends Node + signal new_data_update(data) signal patch_data_update(data) signal delete_data_update(data) @@ -13,188 +14,198 @@ signal delete_data_update(data) 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" - -var _pusher : HTTPRequest -var _listener : Node -var _store : FirebaseDatabaseStore -var _auth : Dictionary -var _config : Dictionary -var _filter_query : Dictionary -var _db_path : String -var _cached_filter : String -var _push_queue : Array = [] -var _update_queue : Array = [] -var _delete_queue : Array = [] -var _can_connect_to_host : bool = false - -const _put_tag : String = "put" -const _patch_tag : String = "patch" -const _delete_tag : String = "delete" -const _separator : String = "/" -const _json_list_tag : String = ".json" -const _query_tag : String = "?" -const _auth_tag : String = "auth=" -const _accept_header : String = "accept: text/event-stream" -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 : PoolStringArray = [] - -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 - -func set_pusher(pusher_ref : HTTPRequest) -> void: - if !_pusher: - _pusher = pusher_ref - add_child(_pusher) - _pusher.connect("request_completed", self, "on_push_request_complete") - -func set_listener(listener_ref : Node) -> void: - if !_listener: - _listener = listener_ref - add_child(_listener) - _listener.connect("new_sse_event", self, "on_new_sse_event") - var base_url = _get_list_url(false).trim_suffix(_separator) - var extended_url = _separator + _db_path + _get_remaining_path(false) - var port = -1 - if Firebase.emulating: - port = int(_config.emulators.ports.realtimeDatabase) - _listener.connect_to_host(base_url, extended_url, port) - -func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) -> void: - if data: - var command = event - if command and command != "keep-alive": - _route_data(command, data.path, data.data) - if command == _put_tag: - if data.path == _separator and data.data and data.data.keys().size() > 0: - for key in data.data.keys(): - emit_signal("new_data_update", FirebaseResource.new(_separator + key, data.data[key])) - elif data.path != _separator: - emit_signal("new_data_update", FirebaseResource.new(data.path, data.data)) - elif command == _patch_tag: - emit_signal("patch_data_update", FirebaseResource.new(data.path, data.data)) - elif command == _delete_tag: - emit_signal("delete_data_update", FirebaseResource.new(data.path, data.data)) - pass - -func set_store(store_ref : FirebaseDatabaseStore) -> void: - if !_store: - _store = store_ref - add_child(_store) - -func update(path : String, data : Dictionary) -> void: - path = path.strip_edges(true, true) - - if path == _separator: - path = "" - - var to_update = JSON.print(data) - var status = _pusher.get_http_client_status() - if status == HTTPClient.STATUS_DISCONNECTED || status != HTTPClient.STATUS_REQUESTING: - var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) - - _pusher.request(resolved_path, _headers, true, HTTPClient.METHOD_PATCH, to_update) - else: - _update_queue.append({"path": path, "data": data}) - -func push(data : Dictionary) -> void: - var to_push = JSON.print(data) - if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, true, HTTPClient.METHOD_POST, to_push) - else: - _push_queue.append(data) - -func delete(reference : String) -> void: - if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: - _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, true, HTTPClient.METHOD_DELETE, "") - else: - _delete_queue.append(reference) - -# -# Returns a deep copy of the current local copy of the data stored at this reference in the Firebase -# Realtime Database. -# +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" + +var _pusher: HTTPRequest +var _listener: Node +var _store: FirebaseDatabaseStore +var _auth: Dictionary +var _config: Dictionary +var _filter_query: Dictionary +var _db_path: String +var _cached_filter: String +var _push_queue: Array = [] +var _update_queue: Array = [] +var _delete_queue: Array = [] +var _can_connect_to_host: bool = false + +const _put_tag: String = "put" +const _patch_tag: String = "patch" +const _delete_tag: String = "delete" +const _separator: String = "/" +const _json_list_tag: String = ".json" +const _query_tag: String = "?" +const _auth_tag: String = "auth=" +const _accept_header: String = "accept: text/event-stream" +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: PoolStringArray = [] + + +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 + + +func set_pusher(pusher_ref: HTTPRequest) -> void: + if not _pusher: + _pusher = pusher_ref + add_child(_pusher) + _pusher.connect("request_completed", self, "on_push_request_complete") + + +func set_listener(listener_ref: Node) -> void: + if not _listener: + _listener = listener_ref + add_child(_listener) + _listener.connect("new_sse_event", self, "on_new_sse_event") + var base_url = _get_list_url(false).trim_suffix(_separator) + var extended_url = _separator + _db_path + _get_remaining_path(false) + var port = -1 + if Firebase.emulating: + port = int(_config.emulators.ports.realtimeDatabase) + _listener.connect_to_host(base_url, extended_url, port) + + +func on_new_sse_event(headers: Dictionary, event: String, data: Dictionary) -> void: + if data: + var command = event + if command and command != "keep-alive": + _route_data(command, data.path, data.data) + if command == _put_tag: + if data.path == _separator and data.data and data.data.keys().size() > 0: + for key in data.data.keys(): + emit_signal("new_data_update", FirebaseResource.new(_separator + key, data.data[key])) + elif data.path != _separator: + emit_signal("new_data_update", FirebaseResource.new(data.path, data.data)) + elif command == _patch_tag: + emit_signal("patch_data_update", FirebaseResource.new(data.path, data.data)) + elif command == _delete_tag: + emit_signal("delete_data_update", FirebaseResource.new(data.path, data.data)) + pass + + +func set_store(store_ref: FirebaseDatabaseStore) -> void: + if not _store: + _store = store_ref + add_child(_store) + + +func update(path: String, data: Dictionary) -> void: + path = path.strip_edges(true, true) + + if path == _separator: + path = "" + + var to_update = JSON.print(data) + var status = _pusher.get_http_client_status() + if status == HTTPClient.STATUS_DISCONNECTED or status != HTTPClient.STATUS_REQUESTING: + var resolved_path = _get_list_url() + _db_path + "/" + path + _get_remaining_path() + + _pusher.request(resolved_path, _headers, true, HTTPClient.METHOD_PATCH, to_update) + else: + _update_queue.append({"path": path, "data": data}) + + +func push(data: Dictionary) -> void: + var to_push = JSON.print(data) + if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: + _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, true, HTTPClient.METHOD_POST, to_push) + else: + _push_queue.append(data) + + +func delete(reference: String) -> void: + if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: + _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, true, HTTPClient.METHOD_DELETE, "") + else: + _delete_queue.append(reference) + + +## 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 not _filter_query or is_push: + remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken + else: + remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken -func _get_remaining_path(is_push : bool = true) -> String: - var remaining_path = "" - if !_filter_query or is_push: - remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken - else: - remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken + 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 +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: - 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 - -# -# Appropriately updates the current local copy of the data stored at this reference in the Firebase -# Realtime Database. -# -func _route_data(command : String, path : String, data) -> void: - if command == _put_tag: - _store.put(path, data) - elif command == _patch_tag: - _store.patch(path, data) - elif command == _delete_tag: - _store.delete(path, data) - -func on_push_request_complete(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - if response_code == HTTPClient.RESPONSE_OK: - emit_signal("push_successful") - else: - emit_signal("push_failed") - - if _push_queue.size() > 0: - push(_push_queue.pop_front()) - return - if _update_queue.size() > 0: - var e = _update_queue.pop_front() - update(e['path'], e['data']) - return - if _delete_queue.size() > 0: - delete(_delete_queue.pop_front()) + if not _filter_query: + return "" + # At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules. + if not _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 + + +## Appropriately updates the current local copy of the data stored at this reference in the Firebase +## Realtime Database. +func _route_data(command: String, path: String, data) -> void: + if command == _put_tag: + _store.put(path, data) + elif command == _patch_tag: + _store.patch(path, data) + elif command == _delete_tag: + _store.delete(path, data) + + +func on_push_request_complete(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray) -> void: + if response_code == HTTPClient.RESPONSE_OK: + emit_signal("push_successful") + else: + emit_signal("push_failed") + + if _push_queue.size() > 0: + push(_push_queue.pop_front()) + return + if _update_queue.size() > 0: + var e = _update_queue.pop_front() + update(e["path"], e["data"]) + return + if _delete_queue.size() > 0: + delete(_delete_queue.pop_front()) diff --git a/addons/godot-firebase/database/resource.gd b/addons/godot-firebase/database/resource.gd index c273206..1c6fcf8 100644 --- a/addons/godot-firebase/database/resource.gd +++ b/addons/godot-firebase/database/resource.gd @@ -5,12 +5,15 @@ tool class_name FirebaseResource extends Resource -var key : String + +var key: String var data -func _init(key : String, data) -> void: - self.key = key.lstrip("/") - self.data = data + +func _init(key: String, data) -> void: + 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 fe6e4a3..e89d880 100644 --- a/addons/godot-firebase/dynamiclinks/dynamiclinks.gd +++ b/addons/godot-firebase/dynamiclinks/dynamiclinks.gd @@ -7,101 +7,108 @@ tool class_name FirebaseDynamicLinks extends Node + signal dynamic_link_generated(link_result) signal generate_dynamic_link_error(error) -const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " -const _API_VERSION : String = "v1" +const _AUTHORIZATION_HEADER: String = "Authorization: Bearer " +const _API_VERSION: String = "v1" -var request : int = -1 +var request: int = -1 -var _base_url : String = "" +var _base_url: String = "" -var _config : Dictionary = {} +var _config: Dictionary = {} -var _auth : Dictionary -var _request_list_node : HTTPRequest +var _auth: Dictionary +var _request_list_node: HTTPRequest -var _headers : PoolStringArray = [] +var _headers: PoolStringArray = [] enum Requests { - NONE = -1, - GENERATE - } - -func _set_config(config_json : Dictionary) -> void: - _config = config_json - _request_list_node = HTTPRequest.new() - _request_list_node.connect("request_completed", self, "_on_request_completed") - add_child(_request_list_node) - _check_emulating() - - -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 }) - - -var _link_request_body : Dictionary = { - "dynamicLinkInfo": { - "domainUriPrefix": "", - "link": "", - "androidInfo": { - "androidPackageName": "" - }, - "iosInfo": { - "iosBundleId": "" - } - }, - "suffix": { - "option": "" - } - } + NONE = -1, + GENERATE +} + + +func _set_config(config_json: Dictionary) -> void: + _config = config_json + _request_list_node = HTTPRequest.new() + _request_list_node.connect("request_completed", self, "_on_request_completed") + add_child(_request_list_node) + _check_emulating() + + +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}) + + +var _link_request_body: Dictionary = { + "dynamicLinkInfo": { + "domainUriPrefix": "", + "link": "", + "androidInfo": { + "androidPackageName": "" + }, + "iosInfo": { + "iosBundleId": "" + } + }, + "suffix": { + "option": "" + } +} + ## @args log_link, APN, IBI, is_unguessable ## This function is used to generate a dynamic link using the Firebase REST API ## It will return a JSON with the shortened link -func generate_dynamic_link(long_link : String, APN : String, IBI : String, is_unguessable : bool) -> void: - if not _config.domainUriPrefix or _config.domainUriPrefix == "": - emit_signal("generate_dynamic_link_error", "You're missing the domainUriPrefix in config file! Error!") - Firebase._printerr("You're missing the domainUriPrefix in config file! Error!") - return - - request = Requests.GENERATE - _link_request_body.dynamicLinkInfo.domainUriPrefix = _config.domainUriPrefix - _link_request_body.dynamicLinkInfo.link = long_link - _link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN - _link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI - if is_unguessable: - _link_request_body.suffix.option = "UNGUESSABLE" - else: - _link_request_body.suffix.option = "SHORT" - _request_list_node.request(_base_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_link_request_body)) - -func _on_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - var result_body = JSON.parse(body.get_string_from_utf8()) - if result_body.error: - emit_signal("generate_dynamic_link_error", result_body.error_string) - return - else: - result_body = result_body.result - - emit_signal("dynamic_link_generated", result_body.shortLink) - request = Requests.NONE - -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 generate_dynamic_link(long_link: String, APN: String, IBI: String, is_unguessable: bool) -> void: + if not _config.domainUriPrefix or _config.domainUriPrefix == "": + emit_signal("generate_dynamic_link_error", "You're missing the domainUriPrefix in config file! Error!") + Firebase._printerr("You're missing the domainUriPrefix in config file! Error!") + return + + request = Requests.GENERATE + _link_request_body.dynamicLinkInfo.domainUriPrefix = _config.domainUriPrefix + _link_request_body.dynamicLinkInfo.link = long_link + _link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN + _link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI + if is_unguessable: + _link_request_body.suffix.option = "UNGUESSABLE" + else: + _link_request_body.suffix.option = "SHORT" + _request_list_node.request(_base_url, _headers, true, HTTPClient.METHOD_POST, JSON.print(_link_request_body)) + + +func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray) -> void: + var result_body = JSON.parse(body.get_string_from_utf8()) + if result_body.error: + emit_signal("generate_dynamic_link_error", result_body.error_string) + return + else: + result_body = result_body.result + + emit_signal("dynamic_link_generated", result_body.shortLink) + request = Requests.NONE + + +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 = {} + _auth = {} diff --git a/addons/godot-firebase/firebase/firebase.gd b/addons/godot-firebase/firebase/firebase.gd index d53d0b7..00ad225 100644 --- a/addons/godot-firebase/firebase/firebase.gd +++ b/addons/godot-firebase/firebase/firebase.gd @@ -11,131 +11,137 @@ tool extends Node -const _ENVIRONMENT_VARIABLES : String = "firebase/environment_variables" -const _EMULATORS_PORTS : String = "firebase/emulators/ports" -const _AUTH_PROVIDERS : String = "firebase/auth_providers" + +const _ENVIRONMENT_VARIABLES: String = "firebase/environment_variables" +const _EMULATORS_PORTS: String = "firebase/emulators/ports" +const _AUTH_PROVIDERS: String = "firebase/auth_providers" ## @type FirebaseAuth ## The Firebase Authentication API. -onready var Auth : FirebaseAuth = $Auth +onready var Auth: FirebaseAuth = $Auth ## @type FirebaseFirestore ## The Firebase Firestore API. -onready var Firestore : FirebaseFirestore = $Firestore +onready var Firestore: FirebaseFirestore = $Firestore ## @type FirebaseDatabase ## The Firebase Realtime Database API. -onready var Database : FirebaseDatabase = $Database +onready var Database: FirebaseDatabase = $Database ## @type FirebaseStorage ## The Firebase Storage API. -onready var Storage : FirebaseStorage = $Storage +onready var Storage: FirebaseStorage = $Storage ## @type FirebaseDynamicLinks ## The Firebase Dynamic Links API. -onready var DynamicLinks : FirebaseDynamicLinks = $DynamicLinks +onready var DynamicLinks: FirebaseDynamicLinks = $DynamicLinks ## @type FirebaseFunctions ## The Firebase Cloud Functions API -onready var Functions : FirebaseFunctions = $Functions +onready var Functions: FirebaseFunctions = $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 -var _config : Dictionary = { - "apiKey": "", - "authDomain": "", - "databaseURL": "", - "projectId": "", - "storageBucket": "", - "messagingSenderId": "", - "appId": "", - "measurementId": "", - "clientId": "", - "clientSecret" : "", - "domainUriPrefix" : "", - "functionsGeoZone" : "", - "cacheLocation":"user://.firebase_cache", - "emulators": { - "ports" : { - "authentication" : "", - "firestore" : "", - "realtimeDatabase" : "", - "functions" : "", - "storage" : "", - "dynamicLinks" : "" - } - }, - "workarounds":{ - "database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329 - }, - "auth_providers": { - "facebook_id":"", - "facebook_secret":"", - "github_id":"", - "github_secret":"", - "twitter_id":"", - "twitter_secret":"" - } +var _config: Dictionary = { + "apiKey": "", + "authDomain": "", + "databaseURL": "", + "projectId": "", + "storageBucket": "", + "messagingSenderId": "", + "appId": "", + "measurementId": "", + "clientId": "", + "clientSecret": "", + "domainUriPrefix": "", + "functionsGeoZone": "", + "cacheLocation": "user://.firebase_cache", + "emulators": { + "ports": { + "authentication": "", + "firestore": "", + "realtimeDatabase": "", + "functions": "", + "storage": "", + "dynamicLinks": "" + } + }, + "workarounds": { + "database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329 + }, + "auth_providers": { + "facebook_id": "", + "facebook_secret": "", + "github_id": "", + "github_secret": "", + "twitter_id": "", + "twitter_secret": "" + } } + func _ready() -> void: - _load_config() + _load_config() + +func set_emulated(emulating: bool = true) -> void: + self.emulating = emulating + _check_emulating() -func set_emulated(emulating : bool = true) -> void: - self.emulating = emulating - _check_emulating() func _check_emulating() -> void: - if emulating: - print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.") - for module in get_children(): - if module.has_method("_check_emulating"): - module._check_emulating() + if emulating: + print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.") + for module in get_children(): + if module.has_method("_check_emulating"): + module._check_emulating() + func _load_config() -> void: - if _config.apiKey != "" and _config.authDomain != "": - pass - else: - var env = ConfigFile.new() - var err = env.load("res://addons/godot-firebase/.env") - if err == OK: - for key in _config.keys(): - if key == "emulators": - for port in _config[key]["ports"].keys(): - _config[key]["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") - if key == "auth_providers": - for provider in _config[key].keys(): - _config[key][provider] = env.get_value(_AUTH_PROVIDERS, provider) - else: - var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") - if value == "": - _print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key) - else: - _config[key] = value - else: - _printerr("Unable to read .env file at path 'res://addons/godot-firebase/.env'") - - _setup_modules() + if _config.apiKey != "" and _config.authDomain != "": + pass + else: + var env = ConfigFile.new() + var err = env.load("res://addons/godot-firebase/.env") + if err == OK: + for key in _config.keys(): + if key == "emulators": + for port in _config[key]["ports"].keys(): + _config[key]["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") + if key == "auth_providers": + for provider in _config[key].keys(): + _config[key][provider] = env.get_value(_AUTH_PROVIDERS, provider) + else: + var value: String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") + if value == "": + _print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key) + else: + _config[key] = value + else: + _printerr("Unable to read .env file at path 'res://addons/godot-firebase/.env'") + + _setup_modules() + func _setup_modules() -> void: - for module in get_children(): - module._set_config(_config) - if not module.has_method("_on_FirebaseAuth_login_succeeded"): - continue - Auth.connect("login_succeeded", module, "_on_FirebaseAuth_login_succeeded") - Auth.connect("signup_succeeded", module, "_on_FirebaseAuth_login_succeeded") - Auth.connect("token_refresh_succeeded", module, "_on_FirebaseAuth_token_refresh_succeeded") - Auth.connect("logged_out", module, "_on_FirebaseAuth_logout") + for module in get_children(): + module._set_config(_config) + if not module.has_method("_on_FirebaseAuth_login_succeeded"): + continue + Auth.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") # ------------- -func _printerr(error : String) -> void: - printerr("[Firebase Error] >> "+error) +func _printerr(error: String) -> void: + printerr("[Firebase Error] >> " + error) + -func _print(msg : String) -> void: - print("[Firebase] >> "+msg) +func _print(msg: String) -> void: + print("[Firebase] >> " + msg) diff --git a/addons/godot-firebase/firestore/firestore.gd b/addons/godot-firebase/firestore/firestore.gd index 156a645..f579ad6 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 on a collection. - QUERY ## Firestore is processing a [code]query()[/code] request on a collection. + NONE = -1, ## Firestore is not processing any request. + LIST, ## Firestore is processing a [code]list()[/code] request on a collection. + QUERY ## Firestore is processing a [code]query()[/code] request on a collection. } # TODO: Implement cache size limit @@ -86,17 +86,17 @@ var _http_request_pool := [] var _offline: bool = false setget _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(i) - request.set_meta("lifetime", lifetime) + for i in range(_http_request_pool.size() - 1, -1, -1): + var request = _http_request_pool[i] + if not request.get_meta("requesting"): + var lifetime: float = request.get_meta("lifetime") + delta + if lifetime > _MAX_POOLED_REQUEST_AGE: + request.queue_free() + _http_request_pool.remove(i) + request.set_meta("lifetime", lifetime) ## Returns a reference collection by its [i]path[/i]. @@ -106,18 +106,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 on your Firestore database. @@ -140,18 +140,18 @@ func collection(path : String) -> FirestoreCollection: ## @arg-types FirestoreQuery ## @return FirestoreTask func query(query : FirestoreQuery) -> FirestoreTask: - var firestore_task : FirestoreTask = FirestoreTask.new() - firestore_task.connect("result_query", self, "_on_result_query") - firestore_task.connect("task_error", self, "_on_task_error") - firestore_task.action = FirestoreTask.Task.TASK_QUERY - var body : Dictionary = { structuredQuery = query.query } - var url : String = _base_url + _extended_url + _query_suffix + var firestore_task : FirestoreTask = FirestoreTask.new() + firestore_task.connect("result_query", self, "_on_result_query") + firestore_task.connect("task_error", self, "_on_task_error") + firestore_task.action = FirestoreTask.Task.TASK_QUERY + var body : Dictionary = { structuredQuery = query.query } + var url : String = _base_url + _extended_url + _query_suffix - firestore_task.data = query - firestore_task._fields = JSON.print(body) - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task + firestore_task.data = query + firestore_task._fields = JSON.print(body) + firestore_task._url = url + _pooled_request(firestore_task) + return firestore_task ## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield on the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal. @@ -169,212 +169,212 @@ 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.connect("listed_documents", self, "_on_listed_documents") - firestore_task.connect("task_error", self, "_on_task_error") - firestore_task.action = FirestoreTask.Task.TASK_LIST - var url : String = _base_url + _extended_url + path - if page_size != 0: - url+="?pageSize="+str(page_size) - if page_token != "": - url+="&pageToken="+page_token - if order_by != "": - url+="&orderBy="+order_by - - firestore_task.data = [path, page_size, page_token, order_by] - firestore_task._url = url - _pooled_request(firestore_task) - return firestore_task + var firestore_task : FirestoreTask = FirestoreTask.new() + firestore_task.connect("listed_documents", self, "_on_listed_documents") + firestore_task.connect("task_error", self, "_on_task_error") + firestore_task.action = FirestoreTask.Task.TASK_LIST + var url : String = _base_url + _extended_url + path + if page_size != 0: + url+="?pageSize="+str(page_size) + if page_token != "": + url+="&pageToken="+page_token + if order_by != "": + url+="&orderBy="+order_by + + firestore_task.data = [path, page_size, page_token, order_by] + firestore_task._url = url + _pooled_request(firestore_task) + return firestore_task func set_networking(value: bool) -> void: - if value: - enable_networking() - else: - disable_networking() + 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: - 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() + 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() 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) - var file := File.new() - if file.file_exists(_cache_loc.plus_file(_CACHE_RECORD_FILE)): - _offline = true - else: - _offline = false + var file := File.new() + if file.file_exists(_cache_loc.plus_file(_CACHE_RECORD_FILE)): + _offline = true + else: + _offline = false - _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, PoolStringArray(), PoolByteArray()) - return - - if not auth and not Firebase.emulating: - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") - if result[0] != 1: - _check_auth_error(result[0], result[1]) - Firebase._print("Client connected as Anonymous") - - if not Firebase.emulating: - task._headers = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - - var http_request : HTTPRequest - for request in _http_request_pool: - if not request.get_meta("requesting"): - http_request = request - break - - if not http_request: - http_request = HTTPRequest.new() - http_request.timeout = 5 - _http_request_pool.append(http_request) - add_child(http_request) - http_request.connect("request_completed", self, "_on_pooled_request_completed", [http_request]) - - http_request.set_meta("requesting", true) - http_request.set_meta("lifetime", 0.0) - http_request.set_meta("task", task) - http_request.request(task._url, task._headers, !Firebase.emulating, task._method, task._fields) + if _offline: + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) + return + + if not auth and not Firebase.emulating: + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = yield(Firebase.Auth, "auth_request") + if result[0] != 1: + _check_auth_error(result[0], result[1]) + Firebase._print("Client connected as Anonymous") + + if not Firebase.emulating: + task._headers = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + + var http_request : HTTPRequest + for request in _http_request_pool: + if not request.get_meta("requesting"): + http_request = request + break + + if not http_request: + http_request = HTTPRequest.new() + http_request.timeout = 5 + _http_request_pool.append(http_request) + add_child(http_request) + http_request.connect("request_completed", self, "_on_pooled_request_completed", [http_request]) + + http_request.set_meta("requesting", true) + http_request.set_meta("lifetime", 0.0) + http_request.set_meta("task", task) + http_request.request(task._url, task._headers, !Firebase.emulating, task._method, task._fields) # ------------- func _on_listed_documents(listed_documents : Array): - emit_signal("listed_documents", listed_documents) + emit_signal("listed_documents", listed_documents) func _on_result_query(result : Array): - emit_signal("result_query", result) + emit_signal("result_query", result) func _on_task_error(code : int, status : String, message : String, task : int): - emit_signal("task_error", code, status, message) - Firebase._printerr(message) + emit_signal("task_error", 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 : PoolStringArray, body : PoolByteArray, 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 7903318..aa9e710 100644 --- a/addons/godot-firebase/firestore/firestore_collection.gd +++ b/addons/godot-firebase/firestore/firestore_collection.gd @@ -36,109 +36,109 @@ var _request_queues := {} ## @return FirestoreTask ## used to GET a document from the collection, specify @document_id func get(document_id : String) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_GET - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_GET + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _separator + document_id.replace(" ", "%20") - task.connect("get_document", self, "_on_get_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task + task.connect("get_document", self, "_on_get_document") + task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) + _process_request(task, document_id, url) + return task ## @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.connect("add_document", self, "_on_add_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.print(FirestoreDocument.dict2fields(fields))) - return task + task.connect("add_document", self, "_on_add_document") + task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.print(FirestoreDocument.dict2fields(fields))) + return task ## @args document_id, fields ## @arg-defaults , {} ## @return FirestoreTask # used to UPDATE a document, specify @documentID and @fields func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: - var task : FirestoreTask = FirestoreTask.new() - task.action = FirestoreTask.Task.TASK_PATCH - task.data = collection_name + "/" + document_id - var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + "?" - for key in fields.keys(): - url+="updateMask.fieldPaths={key}&".format({key = key}) - url = url.rstrip("&") - - task.connect("update_document", self, "_on_update_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url, JSON.print(FirestoreDocument.dict2fields(fields))) - return task + var task : FirestoreTask = FirestoreTask.new() + task.action = FirestoreTask.Task.TASK_PATCH + task.data = collection_name + "/" + document_id + var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + "?" + for key in fields.keys(): + url+="updateMask.fieldPaths={key}&".format({key = key}) + url = url.rstrip("&") + + task.connect("update_document", self, "_on_update_document") + task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) + _process_request(task, document_id, url, JSON.print(FirestoreDocument.dict2fields(fields))) + return task ## @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.connect("delete_document", self, "_on_delete_document") - task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) - _process_request(task, document_id, url) - return task + task.connect("delete_document", self, "_on_delete_document") + task.connect("task_finished", self, "_on_task_finished", [document_id], CONNECT_DEFERRED) + _process_request(task, document_id, url) + return task # ----------------- Functions func _get_request_url() -> String: - return _base_url + _extended_url + collection_name + return _base_url + _extended_url + collection_name func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: - task.connect("task_error", self, "_on_error") - - if not auth: - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") - if result[0] != 1: - Firebase.Firestore._check_auth_error(result[0], result[1]) - return null - Firebase._print("Client authenticated as Anonymous User.") - - task._url = url - task._fields = fields - task._headers = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) - if _request_queues.has(document_id) and not _request_queues[document_id].empty(): - _request_queues[document_id].append(task) - else: - _request_queues[document_id] = [] - firestore._pooled_request(task) -# task._push_request(url, , fields) + task.connect("task_error", self, "_on_error") + + if not auth: + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = yield(Firebase.Auth, "auth_request") + if result[0] != 1: + Firebase.Firestore._check_auth_error(result[0], result[1]) + return null + Firebase._print("Client authenticated as Anonymous User.") + + task._url = url + task._fields = fields + task._headers = PoolStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) + if _request_queues.has(document_id) and not _request_queues[document_id].empty(): + _request_queues[document_id].append(task) + else: + _request_queues[document_id] = [] + firestore._pooled_request(task) +# task._push_request(url, , fields) func _on_task_finished(task : FirestoreTask, document_id : String) -> void: - if not _request_queues[document_id].empty(): - task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) + if not _request_queues[document_id].empty(): + task._push_request(task._url, _AUTHORIZATION_HEADER + auth.idtoken, task._fields) # -------------------- Higher level of communication with signals func _on_get_document(document : FirestoreDocument): - emit_signal("get_document", document ) + emit_signal("get_document", document ) func _on_add_document(document : FirestoreDocument): - emit_signal("add_document", document ) + emit_signal("add_document", document ) func _on_update_document(document : FirestoreDocument): - emit_signal("update_document", document ) + emit_signal("update_document", document ) func _on_delete_document(): - emit_signal("delete_document") + emit_signal("delete_document") func _on_error(code, status, message, task): - emit_signal("error", code, status, message) - Firebase._printerr(message) + emit_signal("error", 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..77f25a9 100644 --- a/addons/godot-firebase/firestore/firestore_document.gd +++ b/addons/godot-firebase/firestore/firestore_document.gd @@ -6,156 +6,164 @@ tool class_name FirestoreDocument extends Reference + # A FirestoreDocument objects that holds all important values for a Firestore Document, # @doc_name = name of the Firestore Document, which is the request PATH # @doc_fields = fields held by Firestore Document, in APIs format # 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 document : Dictionary # the Document itself -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: + self.document = doc + self.doc_name = doc.name + if self.doc_name.count("/") > 2: + self.doc_name = (self.doc_name.split("/") as Array).back() + self.doc_fields = fields2dict(self.document) + self.create_time = doc.createTime -func _init(doc : Dictionary = {}, _doc_name : String = "", _doc_fields : Dictionary = {}) -> void: - self.document = doc - self.doc_name = doc.name - if self.doc_name.count("/") > 2: - self.doc_name = (self.doc_name.split("/") as Array).back() - self.doc_fields = fields2dict(self.document) - self.create_time = doc.createTime # Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields # Field Path using the "dot" (`.`) notation are supported: # ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } } -static func dict2fields(dict : Dictionary) -> Dictionary: - var fields : Dictionary = {} - var var_type : String = "" - for field in dict.keys(): - var field_value = dict[field] - if "." in field: - var keys: Array = field.split(".") - field = keys.pop_front() - keys.invert() - for key in keys: - field_value = { key : field_value } - match typeof(field_value): - TYPE_NIL: var_type = "nullValue" - TYPE_BOOL: var_type = "booleanValue" - TYPE_INT: var_type = "integerValue" - TYPE_REAL: var_type = "doubleValue" - TYPE_STRING: var_type = "stringValue" - TYPE_DICTIONARY: - if is_field_timestamp(field_value): - var_type = "timestampValue" - field_value = dict2timestamp(field_value) - else: - var_type = "mapValue" - field_value = dict2fields(field_value) - TYPE_ARRAY: - var_type = "arrayValue" - field_value = {"values": array2fields(field_value)} - - 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 dict2fields(dict: Dictionary) -> Dictionary: + var fields: Dictionary = {} + var var_type: String = "" + for field in dict.keys(): + var field_value = dict[field] + if "." in field: + var keys: Array = field.split(".") + field = keys.pop_front() + keys.invert() + for key in keys: + field_value = {key: field_value} + match typeof(field_value): + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_REAL: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_DICTIONARY: + if is_field_timestamp(field_value): + var_type = "timestampValue" + field_value = dict2timestamp(field_value) + else: + var_type = "mapValue" + field_value = dict2fields(field_value) + TYPE_ARRAY: + var_type = "arrayValue" + field_value = {"values": array2fields(field_value)} + + 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 +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 + # Pass an Array to parse it to a Firebase arrayValue -static func array2fields(array : Array) -> Array: - var fields : Array = [] - var var_type : String = "" - for field in array: - match typeof(field): - TYPE_DICTIONARY: - if is_field_timestamp(field): - var_type = "timestampValue" - field = dict2timestamp(field) - else: - var_type = "mapValue" - field = dict2fields(field) - TYPE_NIL: var_type = "nullValue" - TYPE_BOOL: var_type = "booleanValue" - TYPE_INT: var_type = "integerValue" - TYPE_REAL: var_type = "doubleValue" - TYPE_STRING: var_type = "stringValue" - TYPE_ARRAY: var_type = "arrayValue" - - fields.append({ var_type : field }) - return fields +static func array2fields(array: Array) -> Array: + var fields: Array = [] + var var_type: String = "" + for field in array: + match typeof(field): + TYPE_DICTIONARY: + if is_field_timestamp(field): + var_type = "timestampValue" + field = dict2timestamp(field) + else: + var_type = "mapValue" + field = dict2fields(field) + TYPE_NIL: var_type = "nullValue" + TYPE_BOOL: var_type = "booleanValue" + TYPE_INT: var_type = "integerValue" + TYPE_REAL: var_type = "doubleValue" + TYPE_STRING: var_type = "stringValue" + TYPE_ARRAY: var_type = "arrayValue" + + 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 +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 OS.get_datetime()) to a Firebase Timestamp -static func dict2timestamp(dict : Dictionary) -> String: - dict.erase('weekday') - dict.erase('dst') - var dict_values : Array = dict.values() - return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values +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 : PoolStringArray = timestamp.split("T")[0].split("-") - dict.append_array(timestamp.split("T")[1].split(":")) - for value in dict.size() : - datetime[datetime.keys()[value]] = int(dict[value]) - return datetime +static func timestamp2dict(timestamp: String) -> Dictionary: + var datetime: Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} + var dict: PoolStringArray = timestamp.split("T")[0].split("-") + dict.append_array(timestamp.split("T")[1].split(":")) + for value in dict.size(): + datetime[datetime.keys()[value]] = int(dict[value]) + return datetime + + +static func is_field_timestamp(field: Dictionary) -> bool: + return field.has_all(["year", "month", "day", "hour", "minute", "second"]) -static func is_field_timestamp(field : Dictionary) -> bool: - 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 a75a692..5cda9e9 100644 --- a/addons/godot-firebase/firestore/firestore_query.gd +++ b/addons/godot-firebase/firestore/firestore_query.gd @@ -7,99 +7,99 @@ extends Reference 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 } func _init(): - return self + return self # Select which fields you want to return as a reflection from your query. # Fields must be added inside a list. Only a field is accepted inside the list # Leave the Array empty if you want to return the whole document func select(fields) -> FirestoreQuery: - match typeof(fields): - TYPE_STRING: - query["select"] = { fields = { fieldPath = fields } } - TYPE_ARRAY: - for field in fields: - field = ({ fieldPath = field }) - query["select"] = { fields = fields } - _: - print("Type of 'fields' is not accepted.") - return self + match typeof(fields): + TYPE_STRING: + query["select"] = { fields = { fieldPath = fields } } + TYPE_ARRAY: + for field in fields: + field = ({ fieldPath = field }) + query["select"] = { fields = fields } + _: + print("Type of 'fields' is not accepted.") + return self # Select the collection you want to return the query result from # if @all_descendants also sub-collections will be returned. If false, only documents will be returned func from(collection_id : String, all_descendants : bool = true) -> FirestoreQuery: - query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}] - return self + query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}] + return self # @collections_array MUST be an Array of Arrays with this structure # [ ["collection_id", true/false] ] func from_many(collections_array : Array) -> FirestoreQuery: - var collections : Array = [] - for collection in collections_array: - collections.append({collectionId = collection[0], allDescendants = collection[1]}) - query["from"] = collections.duplicate(true) - return self + var collections : Array = [] + for collection in collections_array: + collections.append({collectionId = collection[0], allDescendants = collection[1]}) + query["from"] = collections.duplicate(true) + return self # Query the value of a field you want to match @@ -109,43 +109,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. .where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20) func where(field : String, operator : int, value = null, chain : int = -1): - if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: - if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): - var filters : Array = [] - if query.has("where") and query.where.has("compositeFilter"): - if chain == -1: - filters = query.where.compositeFilter.filters.duplicate(true) - chain = OPERATOR.get(query.where.compositeFilter.op) - else: - filters.append(query.where) - filters.append(create_unary_filter(field, operator)) - query["where"] = create_composite_filter(chain, filters) - else: - query["where"] = create_unary_filter(field, operator) - else: - if value == null: - print("A value must be defined to match the field: {field}".format({field = field})) - else: - if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): - var filters : Array = [] - if query.has("where") and query.where.has("compositeFilter"): - if chain == -1: - filters = query.where.compositeFilter.filters.duplicate(true) - chain = OPERATOR.get(query.where.compositeFilter.op) - else: - filters.append(query.where) - filters.append(create_field_filter(field, operator, value)) - query["where"] = create_composite_filter(chain, filters) - else: - query["where"] = create_field_filter(field, operator, value) - return self + if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: + if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): + var filters : Array = [] + if query.has("where") and query.where.has("compositeFilter"): + if chain == -1: + filters = query.where.compositeFilter.filters.duplicate(true) + chain = OPERATOR.get(query.where.compositeFilter.op) + else: + filters.append(query.where) + filters.append(create_unary_filter(field, operator)) + query["where"] = create_composite_filter(chain, filters) + else: + query["where"] = create_unary_filter(field, operator) + else: + if value == null: + print("A value must be defined to match the field: {field}".format({field = field})) + else: + if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): + var filters : Array = [] + if query.has("where") and query.where.has("compositeFilter"): + if chain == -1: + filters = query.where.compositeFilter.filters.duplicate(true) + chain = OPERATOR.get(query.where.compositeFilter.op) + else: + filters.append(query.where) + filters.append(create_field_filter(field, operator, value)) + query["where"] = create_composite_filter(chain, filters) + else: + query["where"] = create_field_filter(field, operator, value) + return self # Order by a field, defining its name and the order direction # default directoin = Ascending func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> FirestoreQuery: - query["orderBy"] = [_order_object(field, direction).obj] - return self + query["orderBy"] = [_order_object(field, direction).obj] + return self # Order by a set of fields and directions @@ -153,88 +153,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 ad65f03..e5e6fee 100644 --- a/addons/godot-firebase/firestore/firestore_task.gd +++ b/addons/godot-firebase/firestore/firestore_task.gd @@ -47,12 +47,12 @@ 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 } ## The code indicating the request Firestore is processing. @@ -76,297 +76,297 @@ var _fields : String = "" var _headers : PoolStringArray = [] #func _ready() -> void: -# connect("request_completed", self, "_on_request_completed") +# 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) +# _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) +# if Firebase.Firestore._offline: +# call_deferred("_on_request_completed", -1, 404, PoolStringArray(), PoolByteArray()) +# else: +# request(_url, _headers, true, _method, _fields) func _on_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray) -> void: - var bod - if validate_json(body.get_string_from_utf8()).empty(): - bod = JSON.parse(body.get_string_from_utf8()).result - - var offline: bool = typeof(bod) == TYPE_NIL - var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK - from_cache = offline - - Firebase.Firestore._set_offline(offline) - - var cache_path : String = Firebase._config["cacheLocation"] - if not cache_path.empty() and not failed and Firebase.Firestore.persistence_enabled: - var encrypt_key: String = Firebase.Firestore._encrypt_key - var full_path : String - var url_segment : String - match action: - Task.TASK_LIST: - url_segment = data[0] - full_path = cache_path - Task.TASK_QUERY: - url_segment = JSON.print(data.query) - full_path = cache_path - _: - url_segment = to_json(data) - full_path = _get_doc_file(cache_path, url_segment, encrypt_key) - bod = _handle_cache(offline, data, encrypt_key, full_path, bod) - if not bod.empty() and offline: - response_code = HTTPClient.RESPONSE_OK - - if response_code == HTTPClient.RESPONSE_OK: - data = bod - match action: - Task.TASK_POST: - document = FirestoreDocument.new(bod) - emit_signal("add_document", document) - Task.TASK_GET: - document = FirestoreDocument.new(bod) - emit_signal("get_document", document) - Task.TASK_PATCH: - document = FirestoreDocument.new(bod) - emit_signal("update_document", document) - Task.TASK_DELETE: - emit_signal("delete_document") - Task.TASK_QUERY: - data = [] - for doc in bod: - if doc.has('document'): - data.append(FirestoreDocument.new(doc.document)) - emit_signal("result_query", data) - Task.TASK_LIST: - data = [] - if bod.has('documents'): - for doc in bod.documents: - data.append(FirestoreDocument.new(doc)) - if bod.has("nextPageToken"): - data.append(bod.nextPageToken) - emit_signal("listed_documents", data) - else: - Firebase._printerr("Action in error was: " + str(action)) - emit_error("task_error", bod, action) - - emit_signal("task_finished", self) + var bod + if validate_json(body.get_string_from_utf8()).empty(): + bod = JSON.parse(body.get_string_from_utf8()).result + + var offline: bool = typeof(bod) == TYPE_NIL + var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK + from_cache = offline + + Firebase.Firestore._set_offline(offline) + + var cache_path : String = Firebase._config["cacheLocation"] + if not cache_path.empty() and not failed and Firebase.Firestore.persistence_enabled: + var encrypt_key: String = Firebase.Firestore._encrypt_key + var full_path : String + var url_segment : String + match action: + Task.TASK_LIST: + url_segment = data[0] + full_path = cache_path + Task.TASK_QUERY: + url_segment = JSON.print(data.query) + full_path = cache_path + _: + url_segment = to_json(data) + full_path = _get_doc_file(cache_path, url_segment, encrypt_key) + bod = _handle_cache(offline, data, encrypt_key, full_path, bod) + if not bod.empty() and offline: + response_code = HTTPClient.RESPONSE_OK + + if response_code == HTTPClient.RESPONSE_OK: + data = bod + match action: + Task.TASK_POST: + document = FirestoreDocument.new(bod) + emit_signal("add_document", document) + Task.TASK_GET: + document = FirestoreDocument.new(bod) + emit_signal("get_document", document) + Task.TASK_PATCH: + document = FirestoreDocument.new(bod) + emit_signal("update_document", document) + Task.TASK_DELETE: + emit_signal("delete_document") + Task.TASK_QUERY: + data = [] + for doc in bod: + if doc.has('document'): + data.append(FirestoreDocument.new(doc.document)) + emit_signal("result_query", data) + Task.TASK_LIST: + data = [] + if bod.has('documents'): + for doc in bod.documents: + data.append(FirestoreDocument.new(doc)) + if bod.has("nextPageToken"): + data.append(bod.nextPageToken) + emit_signal("listed_documents", data) + else: + Firebase._printerr("Action in error was: " + str(action)) + emit_error("task_error", bod, action) + + emit_signal("task_finished", self) func emit_error(signal_name : String, bod, task) -> void: - if bod: - if bod is Array and bod.size() > 0 and bod[0].has("error"): - error = bod[0].error - elif bod is Dictionary and bod.keys().size() > 0 and bod.has("error"): - error = bod.error + 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) + emit_signal(signal_name, error.code, error.status, error.message, task) - return + return - emit_signal(signal_name, 1, 0, "Unknown error", task) + emit_signal(signal_name, 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: - 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 + 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 func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: - var ret := dic_a.duplicate(true) - for key in dic_b: - var val = dic_b[key] + var ret := dic_a.duplicate(true) + for key in dic_b: + var val = dic_b[key] - if val == null and nullify: - ret.erase(key) - elif val is Array: - ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val) - elif val is Dictionary: - ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val) - else: - ret[key] = val - return ret + if val == null and nullify: + ret.erase(key) + elif val is Array: + ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val) + elif val is Dictionary: + ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val) + else: + ret[key] = val + return ret func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: - var ret := arr_a.duplicate(true) - ret.resize(len(arr_b)) - - var deletions := 0 - for i in len(arr_b): - var index : int = i - deletions - var val = arr_b[index] - if val == null and nullify: - ret.remove(index) - deletions += i - elif val is Array: - ret[index] = _merge_array(ret[index] if ret[index] else [], val) - elif val is Dictionary: - ret[index] = _merge_dict(ret[index] if ret[index] else {}, val) - else: - ret[index] = val - return ret + var ret := arr_a.duplicate(true) + ret.resize(len(arr_b)) + + var deletions := 0 + for i in len(arr_b): + var index : int = i - deletions + var val = arr_b[index] + if val == null and nullify: + ret.remove(index) + deletions += i + elif val is Array: + ret[index] = _merge_array(ret[index] if ret[index] else [], val) + elif val is Dictionary: + ret[index] = _merge_dict(ret[index] if ret[index] else {}, val) + else: + ret[index] = val + return ret static func _get_doc_file(cache_path : String, document_id : String, encrypt_key : String) -> String: - var file := File.new() - var path := "" - var i = 0 - while i < 256: - path = cache_path.plus_file("%s-%d.fscache" % [str(document_id.hash()).pad_zeros(10), i]) - if file.file_exists(path): - var is_file := false - if file.open_encrypted_with_pass(path, File.READ, encrypt_key) == OK: - is_file = file.get_line() == document_id - file.close() - - if is_file: - return path - else: - i += 1 - else: - return path - return path + var 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..811e5ba 100644 --- a/addons/godot-firebase/functions/function_task.gd +++ b/addons/godot-firebase/functions/function_task.gd @@ -9,11 +9,11 @@ ## [code]var result : Array = yield(Firebase.Firestore, "result_query")[/code] ## ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask - tool class_name FunctionTask extends Reference + ## 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(result) @@ -31,41 +31,42 @@ var data: Dictionary var error: Dictionary ## Whether the data came from cache. -var from_cache : bool = false +var from_cache: bool = false -var _response_headers : PoolStringArray = PoolStringArray() -var _response_code : int = 0 +var _response_headers: PoolStringArray = PoolStringArray() +var _response_code: int = 0 -var _method : int = -1 -var _url : String = "" -var _fields : String = "" -var _headers : PoolStringArray = [] +var _method: int = -1 +var _url: String = "" +var _fields: String = "" +var _headers: PoolStringArray = [] -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()} - var offline: bool = typeof(bod) == TYPE_NIL - from_cache = offline +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()} - data = bod - if response_code == HTTPClient.RESPONSE_OK and data!=null: - emit_signal("function_executed", result, data) - else: - error = {result=result, response_code=response_code, data=data} - emit_signal("task_error", result, response_code, str(data)) + var offline: bool = typeof(bod) == TYPE_NIL + from_cache = offline + + data = bod + if response_code == HTTPClient.RESPONSE_OK and data != null: + emit_signal("function_executed", result, data) + else: + error = {result = result, response_code = response_code, data = data} + emit_signal("task_error", result, response_code, str(data)) + + emit_signal("task_finished", data) - emit_signal("task_finished", data) -# #func _handle_cache(offline : bool, data, encrypt_key : String, cache_path : String, body) -> Dictionary: -# if offline: -# Firebase._printerr("Offline queries are currently unsupported!") +# if offline: +# Firebase._printerr("Offline queries are currently unsupported!") # -# if not offline: -# return body -# else: -# return body_return +# if not offline: +# return body +# else: +# return body_return diff --git a/addons/godot-firebase/functions/functions.gd b/addons/godot-firebase/functions/functions.gd index 0bb600e..5aa32b7 100644 --- a/addons/godot-firebase/functions/functions.gd +++ b/addons/godot-firebase/functions/functions.gd @@ -51,168 +51,168 @@ var _http_request_pool : Array = [] var _offline: bool = false setget _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(i) - request.set_meta("lifetime", lifetime) + for i in range(_http_request_pool.size() - 1, -1, -1): + var request = _http_request_pool[i] + if not request.get_meta("requesting"): + var lifetime: float = request.get_meta("lifetime") + delta + if lifetime > _MAX_POOLED_REQUEST_AGE: + request.queue_free() + _http_request_pool.remove(i) + request.set_meta("lifetime", lifetime) ## @args ## @return FunctionTask func execute(function: String, method: int, params: Dictionary = {}, body: Dictionary = {}) -> FunctionTask: - var function_task : FunctionTask = FunctionTask.new() - function_task.connect("task_error", self, "_on_task_error") - function_task.connect("task_finished", self, "_on_task_finished") - function_task.connect("function_executed", self, "_on_function_executed") + 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._method = method + function_task._method = method - var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function + var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function - if not params.empty(): - url += "?" - for key in params.keys(): - url += key + "=" + params[key] + "&" - - function_task._url = url + if not params.empty(): + url += "?" + for key in params.keys(): + url += key + "=" + params[key] + "&" + + function_task._url = url - if not body.empty(): - function_task._headers = PoolStringArray(["Content-Type: application/json"]) - function_task._fields = to_json(body) + if not body.empty(): + function_task._headers = PoolStringArray(["Content-Type: application/json"]) + function_task._fields = to_json(body) - _pooled_request(function_task) - return function_task + _pooled_request(function_task) + return function_task func set_networking(value: bool) -> void: - if value: - enable_networking() - else: - disable_networking() + if value: + enable_networking() + else: + disable_networking() func enable_networking() -> void: - if networking: - return - networking = true - _base_url = _base_url.replace("storeoffline", "functions") + if networking: + return + networking = true + _base_url = _base_url.replace("storeoffline", "functions") func disable_networking() -> void: - if not networking: - return - networking = false - # Pointing to an invalid url should do the trick. - _base_url = _base_url.replace("functions", "storeoffline") + if not networking: + return + networking = false + # Pointing to an invalid url should do the trick. + _base_url = _base_url.replace("functions", "storeoffline") func _set_offline(value: bool) -> void: - if value == _offline: - return + if value == _offline: + return - _offline = value - if not persistence_enabled: - return + _offline = value + if not persistence_enabled: + return - return + return func _set_config(config_json : Dictionary) -> void: - _config = config_json - _cache_loc = _config["cacheLocation"] + _config = config_json + _cache_loc = _config["cacheLocation"] - if _encrypt_key == "": _encrypt_key = _config.apiKey - _check_emulating() + if _encrypt_key == "": _encrypt_key = _config.apiKey + _check_emulating() func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://{zone}-{projectId}.cloudfunctions.net/".format({ zone = _config.functionsGeoZone, projectId = _config.projectId }) - else: - var port : String = _config.emulators.ports.functions - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Cloud Functions has not been configured.") - else: - _base_url = "http://localhost:{port}/{projectId}/{zone}/".format({ port = port, zone = _config.functionsGeoZone, projectId = _config.projectId }) + ## Check emulating + if not Firebase.emulating: + _base_url = "https://{zone}-{projectId}.cloudfunctions.net/".format({ zone = _config.functionsGeoZone, projectId = _config.projectId }) + else: + var port : String = _config.emulators.ports.functions + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Cloud Functions has not been configured.") + else: + _base_url = "http://localhost:{port}/{projectId}/{zone}/".format({ port = port, zone = _config.functionsGeoZone, projectId = _config.projectId }) func _pooled_request(task : FunctionTask) -> void: - if _offline: - task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) - return + if _offline: + task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PoolStringArray(), PoolByteArray()) + return - if not auth: - Firebase._print("Unauthenticated request issued...") - Firebase.Auth.login_anonymous() - var result : Array = yield(Firebase.Auth, "auth_request") - if result[0] != 1: - _check_auth_error(result[0], result[1]) - Firebase._print("Client connected as Anonymous") + if not auth: + Firebase._print("Unauthenticated request issued...") + Firebase.Auth.login_anonymous() + var result : Array = yield(Firebase.Auth, "auth_request") + if result[0] != 1: + _check_auth_error(result[0], result[1]) + Firebase._print("Client connected as Anonymous") - 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.connect("request_completed", self, "_on_pooled_request_completed", [http_request]) + if not http_request: + http_request = HTTPRequest.new() + _http_request_pool.append(http_request) + add_child(http_request) + http_request.connect("request_completed", self, "_on_pooled_request_completed", [http_request]) - http_request.set_meta("requesting", true) - http_request.set_meta("lifetime", 0.0) - http_request.set_meta("task", task) - http_request.request(task._url, task._headers, true, task._method, task._fields) + http_request.set_meta("requesting", true) + http_request.set_meta("lifetime", 0.0) + http_request.set_meta("task", task) + http_request.request(task._url, task._headers, true, task._method, task._fields) # ------------- func _on_task_finished(data : Dictionary) : - pass + pass func _on_function_executed(result : int, data : Dictionary) : - pass + pass func _on_task_error(code : int, status : int, message : String): - emit_signal("task_error", code, status, message) - Firebase._printerr(message) + emit_signal("task_error", code, status, message) + Firebase._printerr(message) func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: - auth = auth_result + auth = auth_result func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - auth = auth_result + auth = auth_result func _on_pooled_request_completed(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest) -> void: - request.get_meta("task")._on_request_completed(result, response_code, headers, body) - request.set_meta("requesting", false) + 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 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.gd b/addons/godot-firebase/plugin.gd index d21b2e2..30d6517 100644 --- a/addons/godot-firebase/plugin.gd +++ b/addons/godot-firebase/plugin.gd @@ -1,8 +1,10 @@ tool extends EditorPlugin + func _enter_tree() -> void: - add_autoload_singleton("Firebase", "res://addons/godot-firebase/firebase/firebase.tscn") + add_autoload_singleton("Firebase", "res://addons/godot-firebase/firebase/firebase.tscn") + func _exit_tree() -> void: - remove_autoload_singleton("Firebase") + remove_autoload_singleton("Firebase") diff --git a/addons/godot-firebase/storage/storage.gd b/addons/godot-firebase/storage/storage.gd index 56da0ea..2140800 100644 --- a/addons/godot-firebase/storage/storage.gd +++ b/addons/godot-firebase/storage/storage.gd @@ -8,7 +8,8 @@ tool class_name FirebaseStorage extends Node -const _API_VERSION : String = "v0" + +const _API_VERSION: String = "v0" ## @arg-types int, int, PoolStringArray ## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode @@ -21,333 +22,346 @@ signal task_successful(result, response_code, data) signal task_failed(result, response_code, data) ## The current storage bucket the Storage API is referencing. -var bucket : String +var bucket: String ## @default false ## Whether a task is currently being processed. -var requesting : bool = false - -var _auth : Dictionary -var _config : Dictionary - -var _references : Dictionary = {} - -var _base_url : String = "" -var _extended_url : String = "/[API_VERSION]/b/[APP_ID]/o/[FILE_PATH]" -var _root_ref : StorageReference - -var _http_client : HTTPClient = HTTPClient.new() -var _pending_tasks : Array = [] - -var _current_task : StorageTask -var _response_code : int -var _response_headers : PoolStringArray -var _response_data : PoolByteArray -var _content_length : int -var _reading_body : bool - -func _notification(what : int) -> void: - if what == NOTIFICATION_INTERNAL_PROCESS: - _internal_process(get_process_delta_time()) - -func _internal_process(_delta : float) -> void: - if not requesting: - set_process_internal(false) - return - - var task = _current_task - - match _http_client.get_status(): - HTTPClient.STATUS_DISCONNECTED: - _http_client.connect_to_host(_base_url, 443, true) - - HTTPClient.STATUS_RESOLVING, \ - HTTPClient.STATUS_REQUESTING, \ - HTTPClient.STATUS_CONNECTING: - _http_client.poll() - - HTTPClient.STATUS_CONNECTED: - var err := _http_client.request_raw(task._method, task._url, task._headers, task.data) - if err: - _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) - - HTTPClient.STATUS_BODY: - if _http_client.has_response() or _reading_body: - _reading_body = true - - # If there is a response... - if _response_headers.empty(): - _response_headers = _http_client.get_response_headers() # Get response headers. - _response_code = _http_client.get_response_code() - - for header in _response_headers: - if "Content-Length" in header: - _content_length = header.trim_prefix("Content-Length: ").to_int() - - _http_client.poll() - var chunk = _http_client.read_response_body_chunk() # Get a chunk. - if chunk.size() == 0: - # Got nothing, wait for buffers to fill a bit. - pass - else: - _response_data += chunk # Append to read buffer. - if _content_length != 0: - task.progress = float(_response_data.size()) / _content_length - - if _http_client.get_status() != HTTPClient.STATUS_BODY: - task.progress = 1.0 - _finish_request(HTTPRequest.RESULT_SUCCESS) - else: - task.progress = 1.0 - _finish_request(HTTPRequest.RESULT_SUCCESS) - - HTTPClient.STATUS_CANT_CONNECT: - _finish_request(HTTPRequest.RESULT_CANT_CONNECT) - HTTPClient.STATUS_CANT_RESOLVE: - _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) - HTTPClient.STATUS_CONNECTION_ERROR: - _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) - HTTPClient.STATUS_SSL_HANDSHAKE_ERROR: - _finish_request(HTTPRequest.RESULT_SSL_HANDSHAKE_ERROR) +var requesting: bool = false + +var _auth: Dictionary +var _config: Dictionary + +var _references: Dictionary = {} + +var _base_url: String = "" +var _extended_url: String = "/[API_VERSION]/b/[APP_ID]/o/[FILE_PATH]" +var _root_ref: StorageReference + +var _http_client: HTTPClient = HTTPClient.new() +var _pending_tasks: Array = [] + +var _current_task: StorageTask +var _response_code: int +var _response_headers: PoolStringArray +var _response_data: PoolByteArray +var _content_length: int +var _reading_body: bool + + +func _notification(what: int) -> void: + if what == NOTIFICATION_INTERNAL_PROCESS: + _internal_process(get_process_delta_time()) + + +func _internal_process(_delta: float) -> void: + if not requesting: + set_process_internal(false) + return + + var task = _current_task + + match _http_client.get_status(): + HTTPClient.STATUS_DISCONNECTED: + _http_client.connect_to_host(_base_url, 443, true) + + HTTPClient.STATUS_RESOLVING, \ + HTTPClient.STATUS_REQUESTING, \ + HTTPClient.STATUS_CONNECTING: + _http_client.poll() + + HTTPClient.STATUS_CONNECTED: + var err := _http_client.request_raw(task._method, task._url, task._headers, task.data) + if err: + _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) + + HTTPClient.STATUS_BODY: + if _http_client.has_response() or _reading_body: + _reading_body = true + + # If there is a response... + if _response_headers.empty(): + _response_headers = _http_client.get_response_headers() # Get response headers. + _response_code = _http_client.get_response_code() + + for header in _response_headers: + if "Content-Length" in header: + _content_length = header.trim_prefix("Content-Length: ").to_int() + + _http_client.poll() + var chunk = _http_client.read_response_body_chunk() # Get a chunk. + if chunk.size() == 0: + # Got nothing, wait for buffers to fill a bit. + pass + else: + _response_data += chunk # Append to read buffer. + if _content_length != 0: + task.progress = float(_response_data.size()) / _content_length + + if _http_client.get_status() != HTTPClient.STATUS_BODY: + task.progress = 1.0 + _finish_request(HTTPRequest.RESULT_SUCCESS) + else: + task.progress = 1.0 + _finish_request(HTTPRequest.RESULT_SUCCESS) + + HTTPClient.STATUS_CANT_CONNECT: + _finish_request(HTTPRequest.RESULT_CANT_CONNECT) + HTTPClient.STATUS_CANT_RESOLVE: + _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) + HTTPClient.STATUS_CONNECTION_ERROR: + _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) + HTTPClient.STATUS_SSL_HANDSHAKE_ERROR: + _finish_request(HTTPRequest.RESULT_SSL_HANDSHAKE_ERROR) + ## @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. func ref(path := "") -> StorageReference: - if not _config: - return null - - # Create a root storage reference if there's none - # and we're not making one. - if path != "" and not _root_ref: - _root_ref = ref() - - path = _simplify_path(path) - if not _references.has(path): - var ref := StorageReference.new() - _references[path] = ref - ref.valid = true - ref.bucket = bucket - ref.full_path = path - ref.name = path.get_file() - ref.parent = ref(path.plus_file("..")) - ref.root = _root_ref - ref.storage = self - return ref - else: - return _references[path] - -func _set_config(config_json : Dictionary) -> void: - _config = config_json - if bucket != _config.storageBucket: - bucket = _config.storageBucket - _http_client.close() - _check_emulating() - - -func _check_emulating() -> void : - ## Check emulating - if not Firebase.emulating: - _base_url = "https://firebasestorage.googleapis.com" - else: - var port : String = _config.emulators.ports.storage - if port == "": - Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.") - else: - _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) - - -func _upload(data : PoolByteArray, headers : PoolStringArray, ref : StorageReference, meta_only : bool) -> StorageTask: - if not (_config and _auth): - return null - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) - task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD - task._headers = headers - task.data = data - _process_request(task) - return task - -func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> StorageTask: - if not (_config and _auth): - return null - - var info_task := StorageTask.new() - info_task.ref = ref - info_task._url = _get_file_url(ref) - info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META - _process_request(info_task) - - if url_only or meta_only: - return info_task - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) + "?alt=media&token=" - task.action = StorageTask.Task.TASK_DOWNLOAD - _pending_tasks.append(task) - - yield(info_task, "task_finished") - if info_task.data and not info_task.data.has("error"): - task._url += info_task.data.downloadTokens - else: - task.data = info_task.data - task.response_headers = info_task.response_headers - task.response_code = info_task.response_code - task.result = info_task.result - task.finished = true - task.emit_signal("task_finished") - emit_signal("task_failed", task.result, task.response_code, task.data) - _pending_tasks.erase(task) - - return task - -func _list(ref : StorageReference, list_all : bool) -> StorageTask: - if not (_config and _auth): - return null - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(_root_ref).trim_suffix("/") - task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST - _process_request(task) - return task - -func _delete(ref : StorageReference) -> StorageTask: - if not (_config and _auth): - return null - - var task := StorageTask.new() - task.ref = ref - task._url = _get_file_url(ref) - task.action = StorageTask.Task.TASK_DELETE - _process_request(task) - return task - -func _process_request(task : StorageTask) -> void: - if requesting: - _pending_tasks.append(task) - return - requesting = true - - var headers = Array(task._headers) - headers.append("Authorization: Bearer " + _auth.idtoken) - task._headers = PoolStringArray(headers) - - _current_task = task - _response_code = 0 - _response_headers = PoolStringArray() - _response_data = PoolByteArray() - _content_length = 0 - _reading_body = false - - if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]: - _http_client.close() - set_process_internal(true) - -func _finish_request(result : int) -> void: - var task := _current_task - requesting = false - - task.result = result - task.response_code = _response_code - task.response_headers = _response_headers - - match task.action: - StorageTask.Task.TASK_DOWNLOAD: - task.data = _response_data - - StorageTask.Task.TASK_DELETE: - _references.erase(task.ref.full_path) - task.ref.valid = false - if typeof(task.data) == TYPE_RAW_ARRAY: - task.data = null - - StorageTask.Task.TASK_DOWNLOAD_URL: - var json : Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result - if json and json.has("downloadTokens"): - task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens - else: - task.data = "" - - StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: - var json : Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result - var items := [] - if json and json.has("items"): - for item in json.items: - var item_name : String = item.name - if item.bucket != bucket: - continue - if not item_name.begins_with(task.ref.full_path): - continue - if task.action == StorageTask.Task.TASK_LIST: - var dir_path : Array = item_name.split("/") - var slash_count : int = task.ref.full_path.count("/") - item_name = "" - for i in slash_count + 1: - item_name += dir_path[i] - if i != slash_count and slash_count != 0: - item_name += "/" - if item_name in items: - continue - - items.append(item_name) - task.data = items - - _: - task.data = JSON.parse(_response_data.get_string_from_utf8()).result - - var next_task : StorageTask - if not _pending_tasks.empty(): - next_task = _pending_tasks.pop_front() - - task.finished = true - task.emit_signal("task_finished") - if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): - emit_signal("task_failed", task.result, task.response_code, task.data) - else: - emit_signal("task_successful", task.result, task.response_code, task.data) - - while true: - if next_task and not next_task.finished: - _process_request(next_task) - break - elif not _pending_tasks.empty(): - next_task = _pending_tasks.pop_front() - else: - break - - -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")) + if not _config: + return null + + # Create a root storage reference if there's none + # and we're not making one. + if path != "" and not _root_ref: + _root_ref = ref() + + path = _simplify_path(path) + if not _references.has(path): + var ref := StorageReference.new() + _references[path] = ref + ref.valid = true + ref.bucket = bucket + ref.full_path = path + ref.name = path.get_file() + ref.parent = ref(path.plus_file("..")) + ref.root = _root_ref + ref.storage = self + return ref + else: + return _references[path] + + +func _set_config(config_json: Dictionary) -> void: + _config = config_json + if bucket != _config.storageBucket: + bucket = _config.storageBucket + _http_client.close() + _check_emulating() + + +func _check_emulating() -> void: + ## Check emulating + if not Firebase.emulating: + _base_url = "https://firebasestorage.googleapis.com" + else: + var port: String = _config.emulators.ports.storage + if port == "": + Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.") + else: + _base_url = "http://localhost:{port}/{version}/".format({version = _API_VERSION, port = port}) + + +func _upload(data: PoolByteArray, headers: PoolStringArray, ref: StorageReference, meta_only: bool) -> StorageTask: + if not (_config and _auth): + return null + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD + task._headers = headers + task.data = data + _process_request(task) + return task + + +func _download(ref: StorageReference, meta_only: bool, url_only: bool) -> StorageTask: + if not (_config and _auth): + return null + + var info_task := StorageTask.new() + info_task.ref = ref + info_task._url = _get_file_url(ref) + info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META + _process_request(info_task) + + if url_only or meta_only: + return info_task + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + "?alt=media&token=" + task.action = StorageTask.Task.TASK_DOWNLOAD + _pending_tasks.append(task) + + yield(info_task, "task_finished") + if info_task.data and not info_task.data.has("error"): + task._url += info_task.data.downloadTokens + else: + task.data = info_task.data + task.response_headers = info_task.response_headers + task.response_code = info_task.response_code + task.result = info_task.result + task.finished = true + task.emit_signal("task_finished") + emit_signal("task_failed", task.result, task.response_code, task.data) + _pending_tasks.erase(task) + + return task + + +func _list(ref: StorageReference, list_all: bool) -> StorageTask: + if not (_config and _auth): + return null + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(_root_ref).trim_suffix("/") + task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST + _process_request(task) + return task + + +func _delete(ref: StorageReference) -> StorageTask: + if not (_config and _auth): + return null + + var task := StorageTask.new() + task.ref = ref + task._url = _get_file_url(ref) + task.action = StorageTask.Task.TASK_DELETE + _process_request(task) + return task + + +func _process_request(task: StorageTask) -> void: + if requesting: + _pending_tasks.append(task) + return + requesting = true + + var headers = Array(task._headers) + headers.append("Authorization: Bearer " + _auth.idtoken) + task._headers = PoolStringArray(headers) + + _current_task = task + _response_code = 0 + _response_headers = PoolStringArray() + _response_data = PoolByteArray() + _content_length = 0 + _reading_body = false + + if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]: + _http_client.close() + set_process_internal(true) + + +func _finish_request(result: int) -> void: + var task := _current_task + requesting = false + + task.result = result + task.response_code = _response_code + task.response_headers = _response_headers + + match task.action: + StorageTask.Task.TASK_DOWNLOAD: + task.data = _response_data + + StorageTask.Task.TASK_DELETE: + _references.erase(task.ref.full_path) + task.ref.valid = false + if typeof(task.data) == TYPE_RAW_ARRAY: + task.data = null + + StorageTask.Task.TASK_DOWNLOAD_URL: + var json: Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result + if json and json.has("downloadTokens"): + task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens + else: + task.data = "" + + StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: + var json: Dictionary = JSON.parse(_response_data.get_string_from_utf8()).result + var items := [] + if json and json.has("items"): + for item in json.items: + var item_name: String = item.name + if item.bucket != bucket: + continue + if not item_name.begins_with(task.ref.full_path): + continue + if task.action == StorageTask.Task.TASK_LIST: + var dir_path: Array = item_name.split("/") + var slash_count: int = task.ref.full_path.count("/") + item_name = "" + for i in slash_count + 1: + item_name += dir_path[i] + if i != slash_count and slash_count != 0: + item_name += "/" + if item_name in items: + continue + + items.append(item_name) + task.data = items + + _: + task.data = JSON.parse(_response_data.get_string_from_utf8()).result + + var next_task: StorageTask + if not _pending_tasks.empty(): + next_task = _pending_tasks.pop_front() + + task.finished = true + task.emit_signal("task_finished") + if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): + emit_signal("task_failed", task.result, task.response_code, task.data) + else: + emit_signal("task_successful", task.result, task.response_code, task.data) + + while true: + if next_task and not next_task.finished: + _process_request(next_task) + break + elif not _pending_tasks.empty(): + next_task = _pending_tasks.pop_front() + else: + break + + +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")) + # Removes any "../" or "./" in the file path. -func _simplify_path(path : String) -> String: - var dirs := path.split("/") - var new_dirs := [] - for dir in dirs: - if dir == "..": - new_dirs.pop_back() - elif dir == ".": - pass - else: - new_dirs.push_back(dir) - - var new_path := PoolStringArray(new_dirs).join("/") - new_path = new_path.replace("//", "/") - new_path = new_path.replace("\\", "/") - return new_path - -func _on_FirebaseAuth_login_succeeded(auth_token : Dictionary) -> void: - _auth = auth_token - -func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: - _auth = auth_result +func _simplify_path(path: String) -> String: + var dirs := path.split("/") + var new_dirs := [] + for dir in dirs: + if dir == "..": + new_dirs.pop_back() + elif dir == ".": + pass + else: + new_dirs.push_back(dir) + + var new_path := PoolStringArray(new_dirs).join("/") + new_path = new_path.replace("//", "/") + new_path = new_path.replace("\\", "/") + return new_path + + +func _on_FirebaseAuth_login_succeeded(auth_token: Dictionary) -> void: + _auth = auth_token + + +func _on_FirebaseAuth_token_refresh_succeeded(auth_result: Dictionary) -> void: + _auth = auth_result + func _on_FirebaseAuth_logout() -> void: - _auth = {} + _auth = {} diff --git a/addons/godot-firebase/storage/storage_reference.gd b/addons/godot-firebase/storage/storage_reference.gd index 56a41f5..284ca02 100644 --- a/addons/godot-firebase/storage/storage_reference.gd +++ b/addons/godot-firebase/storage/storage_reference.gd @@ -6,6 +6,7 @@ tool class_name StorageReference extends Reference + ## 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. const DEFAULT_MIME_TYPE = "application/octet-stream" @@ -13,52 +14,52 @@ const DEFAULT_MIME_TYPE = "application/octet-stream" ## A dictionary of common MIME types based on 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 "" ## The stroage bucket this referenced file/folder is located in. -var bucket : String = "" +var bucket: String = "" ## @default "" ## The path to the file/folder relative to [member bucket]. -var full_path : String = "" +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 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]. -var parent : StorageReference +var parent: StorageReference ## The root [StorageReference]. -var root : StorageReference +var root: StorageReference ## @type FirebaseStorage ## The Storage API that created this [StorageReference] to begin with. @@ -67,119 +68,133 @@ 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 +var valid: bool = false + ## @args path ## @return StorageReference ## Returns a reference to another [StorageReference] relative to this one. -func child(path : String) -> StorageReference: - if not valid: - return null - return storage.ref(full_path.plus_file(path)) +func child(path: String) -> StorageReference: + if not valid: + return null + return storage.ref(full_path.plus_file(path)) + ## @args data, metadata ## @return StorageTask ## Makes an attempt to upload data to the referenced file location. Status on this task is found in the returned [StorageTask]. -func put_data(data : PoolByteArray, metadata := {}) -> StorageTask: - if not valid: - return null - if not "Content-Length" in metadata and OS.get_name() != "HTML5": - metadata["Content-Length"] = data.size() +func put_data(data: PoolByteArray, metadata := {}) -> StorageTask: + if not valid: + return null + if not "Content-Length" in metadata and OS.get_name() != "HTML5": + metadata["Content-Length"] = data.size() + + 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(), metadata) +func put_string(data: String, metadata := {}) -> StorageTask: + return put_data(data.to_utf8(), 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()) - file.close() +func put_file(file_path: String, metadata := {}) -> StorageTask: + var file := File.new() + file.open(file_path, File.READ) + var data := file.get_buffer(file.get_len()) + file.close() + + 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 on this task is found in the returned [StorageTask]. func get_data() -> StorageTask: - if not valid: - return null - storage._download(self, false, false) - return storage._pending_tasks[-1] + 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.connect("task_finished", self, "_on_task_finished", [task, "stringify"]) - return task + var task := get_data() + task.connect("task_finished", self, "_on_task_finished", [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]. 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 on 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 on the server end. Status on this task is found in the returned [StorageTask]. -func update_metadata(metadata : Dictionary) -> StorageTask: - if not valid: - return null - var data := JSON.print(metadata).to_utf8() - var headers := PoolStringArray(["Accept: application/json"]) - return storage._upload(data, headers, self, true) +func update_metadata(metadata: Dictionary) -> StorageTask: + if not valid: + return null + var data := JSON.print(metadata).to_utf8() + var headers := PoolStringArray(["Accept: application/json"]) + return storage._upload(data, headers, self, true) + ## @return StorageTask ## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status on this task is found in the returned [StorageTask]. func list() -> StorageTask: - if not valid: - return null - return storage._list(self, false) + 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]. 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 on 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 Reference]" - return string - -func _on_task_finished(task : StorageTask, action : String) -> void: - match action: - "stringify": - if typeof(task.data) == TYPE_RAW_ARRAY: - task.data = task.data.get_string_from_utf8() + var string := "gs://%s/%s" % [bucket, full_path] + if not valid: + string += " [Invalid Reference]" + return string + + +func _on_task_finished(task: StorageTask, action: String) -> void: + match action: + "stringify": + if typeof(task.data) == TYPE_RAW_ARRAY: + task.data = task.data.get_string_from_utf8() diff --git a/addons/godot-firebase/storage/storage_task.gd b/addons/godot-firebase/storage/storage_task.gd index 213b371..26071ca 100644 --- a/addons/godot-firebase/storage/storage_task.gd +++ b/addons/godot-firebase/storage/storage_task.gd @@ -5,16 +5,17 @@ tool class_name StorageTask extends Reference + enum Task { - TASK_UPLOAD, - TASK_UPLOAD_META, - TASK_DOWNLOAD, - TASK_DOWNLOAD_META, - TASK_DOWNLOAD_URL, - TASK_LIST, - TASK_LIST_ALL, - TASK_DELETE, - TASK_MAX ## The number of [enum Task] constants. + TASK_UPLOAD, + TASK_UPLOAD_META, + TASK_DOWNLOAD, + TASK_DOWNLOAD_META, + TASK_DOWNLOAD_URL, + TASK_LIST, + TASK_LIST_ALL, + TASK_DELETE, + TASK_MAX ## The number of [enum Task] constants. } ## Emitted when the task is finished. Returns data depending on the success and action of the task. @@ -28,7 +29,7 @@ var ref # Storage Reference (Can't static type due to cyclic reference) ## @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 setget set_action ## @default PoolByteArray() ## Data that the tracked task will/has returned. @@ -36,16 +37,16 @@ var data = PoolByteArray() # data can be of any type. ## @default 0.0 ## The percentage of data that has been received. -var progress : float = 0.0 +var progress: float = 0.0 ## @default -1 ## @enum HTTPRequest.Result ## The resulting status of the task. Anyting other than [constant HTTPRequest.RESULT_SUCCESS] means an error has occured. -var result : int = -1 +var result: int = -1 ## @default false ## Whether the task is finished processing. -var finished : bool = false +var finished: bool = false ## @default PoolStringArray() ## The returned HTTP response headers. @@ -54,20 +55,21 @@ var response_headers := PoolStringArray() ## @default 0 ## @enum HTTPClient.ResponseCode ## The returned HTTP response code. -var response_code : int = 0 +var response_code: int = 0 + +var _method: int = -1 +var _url: String = "" +var _headers: PoolStringArray = PoolStringArray() -var _method : int = -1 -var _url : String = "" -var _headers : PoolStringArray = PoolStringArray() -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 +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 diff --git a/addons/http-sse-client/HTTPSSEClient.gd b/addons/http-sse-client/HTTPSSEClient.gd index f15b395..871e2cf 100644 --- a/addons/http-sse-client/HTTPSSEClient.gd +++ b/addons/http-sse-client/HTTPSSEClient.gd @@ -1,6 +1,7 @@ tool extends Node + signal new_sse_event(headers, event, data) signal connected signal connection_error(error) @@ -22,104 +23,110 @@ var connection_in_progress = false var is_requested = false var response_body = PoolByteArray() -func connect_to_host(domain : String, url_after_domain : String, port : int = -1, use_ssl : bool = false, verify_host : bool = true): - self.domain = domain - self.url_after_domain = url_after_domain - self.port = port - self.use_ssl = use_ssl - self.verify_host = verify_host - told_to_connect = true + +func connect_to_host(domain: String, url_after_domain: String, port: int = -1, use_ssl: bool = false, verify_host: bool = true): + self.domain = domain + self.url_after_domain = url_after_domain + self.port = port + self.use_ssl = use_ssl + self.verify_host = verify_host + told_to_connect = true + func attempt_to_connect(): - var err = httpclient.connect_to_host(domain, port, use_ssl, verify_host) - if err == OK: - emit_signal("connected") - is_connected = true - else: - emit_signal("connection_error", str(err)) + var err = httpclient.connect_to_host(domain, port, use_ssl, verify_host) + if err == OK: + emit_signal("connected") + is_connected = true + else: + emit_signal("connection_error", str(err)) + 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 = JSON.parse(event_data.data).result - if response_body.size() > 0 and result: # stop here if the value doesn't parse - response_body.resize(0) - emit_signal("new_sse_event", headers, event_data.event, result) - else: - if event_data.event != continue_internal: - response_body.resize(0) + var body = response_body.get_string_from_utf8() + if body: + var event_data = get_event_data(body) + if event_data.event != "keep-alive" and event_data.event != continue_internal: + var result = JSON.parse(event_data.data).result + if response_body.size() > 0 and result: # stop here if the value doesn't parse + response_body.resize(0) + emit_signal("new_sse_event", headers, event_data.event, result) + elif event_data.event != continue_internal: + response_body.resize(0) + func _process(delta): - if !told_to_connect: - return - - if !is_connected: - if !connection_in_progress: - attempt_to_connect() - connection_in_progress = true - return - - httpclient.poll() - var httpclient_status = httpclient.get_status() - if !is_requested: - attempt_to_request(httpclient_status) - return - - if httpclient.has_response() or httpclient_status == HTTPClient.STATUS_BODY: - var headers = httpclient.get_response_headers_as_dictionary() - - if httpclient_status == HTTPClient.STATUS_BODY: - httpclient.poll() - var chunk = httpclient.read_response_body_chunk() - if(chunk.size() == 0): - return - else: - response_body = response_body + chunk - - _parse_response_body(headers) - - elif Firebase.emulating and Firebase._config.workarounds.database_connection_closed_issue: - # Emulation does not send the close connection header currently, so we need to manually read the response body - # see issue https://github.com/firebase/firebase-tools/issues/3329 in firebase-tools - # also comment https://github.com/GodotNuts/GodotFirebase/issues/154#issuecomment-831377763 which explains the issue - while httpclient.connection.get_available_bytes(): - var data = httpclient.connection.get_partial_data(1) - if data[0] == OK: - response_body.append_array(data[1]) - if response_body.size() > 0: - _parse_response_body(headers) - -func get_event_data(body : String) -> Dictionary: - var result = {} - var event_idx = body.find(event_tag) - if event_idx == -1: - result["event"] = continue_internal - return result - assert(event_idx != -1) - var data_idx = body.find(data_tag, event_idx + event_tag.length()) - assert(data_idx != -1) - var event = body.substr(event_idx, data_idx) - event = event.replace(event_tag, "").strip_edges() - assert(event) - assert(event.length() > 0) - result["event"] = event - var data = body.right(data_idx + data_tag.length()).strip_edges() - assert(data) - assert(data.length() > 0) - result["data"] = data - return result + if not told_to_connect: + return + + if not is_connected: + if not connection_in_progress: + attempt_to_connect() + connection_in_progress = true + return + + httpclient.poll() + var httpclient_status = httpclient.get_status() + if not is_requested: + attempt_to_request(httpclient_status) + return + + if httpclient.has_response() or httpclient_status == HTTPClient.STATUS_BODY: + var headers = httpclient.get_response_headers_as_dictionary() + + if httpclient_status == HTTPClient.STATUS_BODY: + httpclient.poll() + var chunk = httpclient.read_response_body_chunk() + if chunk.size() == 0: + return + else: + response_body = response_body + chunk + + _parse_response_body(headers) + + elif Firebase.emulating and Firebase._config.workarounds.database_connection_closed_issue: + # Emulation does not send the close connection header currently, so we need to manually read the response body + # see issue https://github.com/firebase/firebase-tools/issues/3329 in firebase-tools + # also comment https://github.com/GodotNuts/GodotFirebase/issues/154#issuecomment-831377763 which explains the issue + while httpclient.connection.get_available_bytes(): + var data = httpclient.connection.get_partial_data(1) + if data[0] == OK: + response_body.append_array(data[1]) + if response_body.size() > 0: + _parse_response_body(headers) + + +func get_event_data(body: String) -> Dictionary: + var result = {} + var event_idx = body.find(event_tag) + if event_idx == -1: + result["event"] = continue_internal + return result + assert(event_idx != -1) + var data_idx = body.find(data_tag, event_idx + event_tag.length()) + assert(data_idx != -1) + var event = body.substr(event_idx, data_idx) + event = event.replace(event_tag, "").strip_edges() + assert(event) + assert(event.length() > 0) + result["event"] = event + var data = body.right(data_idx + data_tag.length()).strip_edges() + assert(data) + assert(data.length() > 0) + result["data"] = data + return result + func _exit_tree(): - if httpclient: - httpclient.close() + if httpclient: + httpclient.close() diff --git a/addons/http-sse-client/httpsseclient_plugin.gd b/addons/http-sse-client/httpsseclient_plugin.gd index bda975a..d0888ee 100644 --- a/addons/http-sse-client/httpsseclient_plugin.gd +++ b/addons/http-sse-client/httpsseclient_plugin.gd @@ -1,8 +1,10 @@ tool extends EditorPlugin + func _enter_tree(): - add_custom_type("HTTPSSEClient", "Node", preload("HTTPSSEClient.gd"), preload("icon.png")) + add_custom_type("HTTPSSEClient", "Node", preload("HTTPSSEClient.gd"), preload("icon.png")) + func _exit_tree(): - remove_custom_type("HTTPSSEClient") + remove_custom_type("HTTPSSEClient") diff --git a/addons/silicon.util.custom_docs/class_doc_generator.gd b/addons/silicon.util.custom_docs/class_doc_generator.gd index b0f8ab5..e703056 100644 --- a/addons/silicon.util.custom_docs/class_doc_generator.gd +++ b/addons/silicon.util.custom_docs/class_doc_generator.gd @@ -1,6 +1,7 @@ tool extends Reference + var _pending_docs := {} var _docs_queue := [] @@ -283,6 +284,7 @@ func _generate(doc: ClassDocItem) -> String: annotations.clear() return "" + func _create_method_doc(name: String, script: Script = null, method := {}) -> MethodDocItem: if method.empty(): var methods := script.get_script_method_list() diff --git a/addons/silicon.util.custom_docs/doc_exporter/doc_exporter.gd b/addons/silicon.util.custom_docs/doc_exporter/doc_exporter.gd index 0f15a97..6591e71 100644 --- a/addons/silicon.util.custom_docs/doc_exporter/doc_exporter.gd +++ b/addons/silicon.util.custom_docs/doc_exporter/doc_exporter.gd @@ -1,8 +1,9 @@ ## The base class for every document exporter. ## @contribute https://placeholder_contribute.com tool -extends Reference class_name DocExporter +extends Reference + ## @virtual ## @args doc diff --git a/addons/silicon.util.custom_docs/doc_item/argument_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/argument_doc_item.gd index 4155ea3..5107bf1 100644 --- a/addons/silicon.util.custom_docs/doc_item/argument_doc_item.gd +++ b/addons/silicon.util.custom_docs/doc_item/argument_doc_item.gd @@ -1,8 +1,9 @@ ## An object that contains documentation data about an argument of a signal or method. ## @contribute https://placeholder_contribute.com tool -extends DocItem class_name ArgumentDocItem +extends DocItem + ## @default "" ## The default value of the argument. @@ -16,6 +17,7 @@ var enumeration := "" ## The class/built-in type of [member default]. var type := "" + func _init(args := {}) -> void: for arg in args: set(arg, args[arg]) diff --git a/addons/silicon.util.custom_docs/doc_item/class_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/class_doc_item.gd index 49a598a..6293708 100644 --- a/addons/silicon.util.custom_docs/doc_item/class_doc_item.gd +++ b/addons/silicon.util.custom_docs/doc_item/class_doc_item.gd @@ -1,8 +1,9 @@ ## An object that contains documentation data about a class. ## @contribute https://placeholder_contribute.com tool -extends DocItem class_name ClassDocItem +extends DocItem + var base := "" ## The base class this class extends from. var path := "" ## The file location of this class' script. @@ -31,6 +32,7 @@ func _init(args := {}) -> void: for arg in args: set(arg, args[arg]) + ## @args name ## @return MethodDocItem ## Gets a method document called [code]name[/code]. @@ -40,6 +42,7 @@ func get_method_doc(name: String) -> MethodDocItem: return doc return null + ## @args name ## @return PropertyDocItem ## Gets a signal document called [code]name[/code]. @@ -49,6 +52,7 @@ func get_property_doc(name: String) -> PropertyDocItem: return doc return null + ## @args name ## @return SignalDocItem ## Gets a signal document called [code]name[/code]. @@ -58,6 +62,7 @@ func get_signal_doc(name: String) -> SignalDocItem: return doc return null + ## @args name ## @return ConstantlDocItem ## Gets a signal document called [code]name[/code]. diff --git a/addons/silicon.util.custom_docs/doc_item/constant_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/constant_doc_item.gd index f3394a5..5ff678e 100644 --- a/addons/silicon.util.custom_docs/doc_item/constant_doc_item.gd +++ b/addons/silicon.util.custom_docs/doc_item/constant_doc_item.gd @@ -1,8 +1,9 @@ ## An object that contains documentation data about a constant. ## @contribute https://placeholder_contribute.com tool -extends DocItem class_name ConstantDocItem +extends DocItem + ## @default "" ## A description of the constant. diff --git a/addons/silicon.util.custom_docs/doc_item/doc_item.gd b/addons/silicon.util.custom_docs/doc_item/doc_item.gd index 6995533..c1c035a 100644 --- a/addons/silicon.util.custom_docs/doc_item/doc_item.gd +++ b/addons/silicon.util.custom_docs/doc_item/doc_item.gd @@ -1,7 +1,8 @@ ## The base class for all documentation items. tool -extends Reference class_name DocItem +extends Reference + ## @default "" ## The name of the documentation item. diff --git a/addons/silicon.util.custom_docs/doc_item/method_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/method_doc_item.gd index 98d5b9e..756ac4b 100644 --- a/addons/silicon.util.custom_docs/doc_item/method_doc_item.gd +++ b/addons/silicon.util.custom_docs/doc_item/method_doc_item.gd @@ -1,6 +1,7 @@ tool -extends DocItem class_name MethodDocItem +extends DocItem + ## @default "" ## A description of the method. diff --git a/addons/silicon.util.custom_docs/doc_item/property_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/property_doc_item.gd index 3206cf2..d6da050 100644 --- a/addons/silicon.util.custom_docs/doc_item/property_doc_item.gd +++ b/addons/silicon.util.custom_docs/doc_item/property_doc_item.gd @@ -1,8 +1,9 @@ ## An object that contains documentation data about a property. ## @contribute https://placeholder_contribute.com tool -extends DocItem class_name PropertyDocItem +extends DocItem + ## @default "" ## A description of the property. @@ -28,6 +29,7 @@ var setter := "" ## The getter method of the property. var getter := "" + func _init(args := {}) -> void: for arg in args: set(arg, args[arg]) diff --git a/addons/silicon.util.custom_docs/doc_item/signal_doc_item.gd b/addons/silicon.util.custom_docs/doc_item/signal_doc_item.gd index 946a33f..ec775bb 100644 --- a/addons/silicon.util.custom_docs/doc_item/signal_doc_item.gd +++ b/addons/silicon.util.custom_docs/doc_item/signal_doc_item.gd @@ -1,8 +1,9 @@ ## An object that contains documentation data about a signal. ## @contribute https://placeholder_contribute.com tool -extends DocItem class_name SignalDocItem +extends DocItem + ## @default "" ## A description of the signal. @@ -12,6 +13,7 @@ var description := "" ## A list of arguments the signal carries. var args := [] + func _init(args := {}) -> void: for arg in args: set(arg, args[arg]) diff --git a/addons/silicon.util.custom_docs/plugin.gd b/addons/silicon.util.custom_docs/plugin.gd index 27f0d17..bee05c5 100644 --- a/addons/silicon.util.custom_docs/plugin.gd +++ b/addons/silicon.util.custom_docs/plugin.gd @@ -2,22 +2,22 @@ tool extends EditorPlugin # enum { -# SEARCH_CLASS = 1, -# SEARCH_METHOD = 2, -# SEARCH_SIGNAL = 4, -# SEARCH_CONSTANT = 8, -# SEARCH_PROPERTY = 16, -# SEARCH_THEME = 32, -# SEARCH_CASE = 64, -# SEARCH_TREE = 128 +# SEARCH_CLASS = 1, +# SEARCH_METHOD = 2, +# SEARCH_SIGNAL = 4, +# SEARCH_CONSTANT = 8, +# SEARCH_PROPERTY = 16, +# SEARCH_THEME = 32, +# SEARCH_CASE = 64, +# SEARCH_TREE = 128 # } # enum { -# ITEM_CLASS, -# ITEM_METHOD, -# ITEM_SIGNAL, -# ITEM_CONSTANT, -# ITEM_PROPERTY +# ITEM_CLASS, +# ITEM_METHOD, +# ITEM_SIGNAL, +# ITEM_CONSTANT, +# ITEM_PROPERTY # } # var doc_generator := preload("class_doc_generator.gd").new() @@ -45,586 +45,586 @@ extends EditorPlugin # var doc_timer: Timer # func _enter_tree() -> void: -# theme = get_editor_interface().get_base_control().theme -# disabled_color = theme.get_color("disabled_font_color", "Editor") - -# script_editor = get_editor_interface().get_script_editor() -# script_list = find_node_by_class(script_editor, "ItemList") -# script_tabs = get_child_chain(script_editor, [0, 1, 1]) -# search_help = find_node_by_class(script_editor, "EditorHelpSearch") -# search_controls = find_node_by_class(search_help, "LineEdit").get_parent() -# tree = find_node_by_class(search_help, "Tree") - -# if not search_help.is_connected("go_to_help", self, "_on_SearchHelp_go_to_help"): -# search_help.connect("go_to_help", self, "_on_SearchHelp_go_to_help", [], CONNECT_DEFERRED) - -# section_list = ItemList.new() -# section_list.allow_reselect = true -# section_list.size_flags_vertical = Control.SIZE_EXPAND_FILL -# get_child_chain(script_editor, [0, 1, 0, 1]).add_child(section_list) - -# if not section_list.is_connected("item_selected", self, "_on_SectionList_item_selected"): -# section_list.connect("item_selected", self, "_on_SectionList_item_selected") - -# doc_exporter.plugin = self -# doc_exporter.theme = theme -# doc_exporter.editor_settings = get_editor_interface().get_editor_settings() -# doc_exporter.class_docs = class_docs -# doc_exporter.update_theme_vars() -# doc_generator.plugin = self - -# doc_timer = Timer.new() -# doc_timer.wait_time = 0.5 -# add_child(doc_timer) -# doc_timer.start() -# if not doc_timer.is_connected("timeout", self, "_on_DocTimer_timeout"): -# doc_timer.connect("timeout", self, "_on_DocTimer_timeout") - -# # Load opened custom docs from last session. -# var settings_path := get_editor_interface().get_editor_settings().get_project_settings_dir() + "/opened_custom_docs.json" -# var file := File.new() -# if not file.open(settings_path, File.READ): -# var opened_tabs := [] -# for i in script_list.get_item_count(): -# opened_tabs.append(script_tabs.get_child(script_list.get_item_metadata(i)).name) - -# var selected := script_list.get_selected_items() -# var list: Array = JSON.parse(file.get_as_text()).result -# for item in list: -# if not item in opened_tabs: -# search_help.call_deferred("emit_signal", "go_to_help", "class_name:" + item) - -# if not selected.empty(): -# script_list.call_deferred("select", selected[0]) -# script_list.call_deferred("emit_signal", "item_selected", selected[0]) -# file.close() +# theme = get_editor_interface().get_base_control().theme +# disabled_color = theme.get_color("disabled_font_color", "Editor") + +# script_editor = get_editor_interface().get_script_editor() +# script_list = find_node_by_class(script_editor, "ItemList") +# script_tabs = get_child_chain(script_editor, [0, 1, 1]) +# search_help = find_node_by_class(script_editor, "EditorHelpSearch") +# search_controls = find_node_by_class(search_help, "LineEdit").get_parent() +# tree = find_node_by_class(search_help, "Tree") + +# if not search_help.is_connected("go_to_help", self, "_on_SearchHelp_go_to_help"): +# search_help.connect("go_to_help", self, "_on_SearchHelp_go_to_help", [], CONNECT_DEFERRED) + +# section_list = ItemList.new() +# section_list.allow_reselect = true +# section_list.size_flags_vertical = Control.SIZE_EXPAND_FILL +# get_child_chain(script_editor, [0, 1, 0, 1]).add_child(section_list) + +# if not section_list.is_connected("item_selected", self, "_on_SectionList_item_selected"): +# section_list.connect("item_selected", self, "_on_SectionList_item_selected") + +# doc_exporter.plugin = self +# doc_exporter.theme = theme +# doc_exporter.editor_settings = get_editor_interface().get_editor_settings() +# doc_exporter.class_docs = class_docs +# doc_exporter.update_theme_vars() +# doc_generator.plugin = self + +# doc_timer = Timer.new() +# doc_timer.wait_time = 0.5 +# add_child(doc_timer) +# doc_timer.start() +# if not doc_timer.is_connected("timeout", self, "_on_DocTimer_timeout"): +# doc_timer.connect("timeout", self, "_on_DocTimer_timeout") + +# # Load opened custom docs from last session. +# var settings_path := get_editor_interface().get_editor_settings().get_project_settings_dir() + "/opened_custom_docs.json" +# var file := File.new() +# if not file.open(settings_path, File.READ): +# var opened_tabs := [] +# for i in script_list.get_item_count(): +# opened_tabs.append(script_tabs.get_child(script_list.get_item_metadata(i)).name) + +# var selected := script_list.get_selected_items() +# var list: Array = JSON.parse(file.get_as_text()).result +# for item in list: +# if not item in opened_tabs: +# search_help.call_deferred("emit_signal", "go_to_help", "class_name:" + item) + +# if not selected.empty(): +# script_list.call_deferred("select", selected[0]) +# script_list.call_deferred("emit_signal", "item_selected", selected[0]) +# file.close() # func _exit_tree() -> void: -# # Save opened custom docs for next session. -# var settings_path := get_editor_interface().get_editor_settings().get_project_settings_dir() + "/opened_custom_docs.json" -# var file := File.new() -# if not file.open(settings_path, File.WRITE): -# var opened_docs := [] -# for i in script_tabs.get_children(): -# if i.name in class_docs: -# opened_docs.append(i.name) -# file.store_string(JSON.print(opened_docs)) -# file.close() +# # Save opened custom docs for next session. +# var settings_path := get_editor_interface().get_editor_settings().get_project_settings_dir() + "/opened_custom_docs.json" +# var file := File.new() +# if not file.open(settings_path, File.WRITE): +# var opened_docs := [] +# for i in script_tabs.get_children(): +# if i.name in class_docs: +# opened_docs.append(i.name) +# file.store_string(JSON.print(opened_docs)) +# file.close() -# section_list.queue_free() +# section_list.queue_free() # func _process(_delta := 0.0) -> void: -# if not tree: -# _enter_tree() -# if not tree: -# return - -# doc_generator._update() - -# # Update search help tree items -# if tree.get_root(): -# search_flags = search_controls.get_child(3).get_item_id(search_controls.get_child(3).selected) -# search_flags |= SEARCH_CASE * int(search_controls.get_child(1).pressed) -# search_flags |= SEARCH_TREE * int(search_controls.get_child(2).pressed) -# search_term = search_controls.get_child(0).text - -# for name in class_docs: -# if fits_search(name, ITEM_CLASS): -# process_custom_item(name, ITEM_CLASS) - -# for method in class_docs[name].methods: -# if fits_search(method.name, ITEM_METHOD): -# process_custom_item(name + "." + method.name, ITEM_METHOD) - -# for _signal in class_docs[name].signals: -# if fits_search(_signal.name, ITEM_SIGNAL): -# process_custom_item(name + "." + _signal.name, ITEM_SIGNAL) - -# for constant in class_docs[name].constants: -# if fits_search(constant.name, ITEM_CONSTANT): -# process_custom_item(name + "." + constant.name, ITEM_CONSTANT) - -# for property in class_docs[name].properties: -# if fits_search(property.name, ITEM_PROPERTY): -# process_custom_item(name + "." + property.name, ITEM_PROPERTY) - -# var custom_doc_open := false -# var doc_open := false -# for i in script_list.get_item_count(): -# var icon := script_list.get_item_icon(i) -# var text := script_list.get_item_text(i) - -# var editor_help = script_tabs.get_child(script_list.get_item_metadata(i)) -# if icon == theme.get_icon("Help", "EditorIcons"): -# if script_list.get_selected_items()[0] == i: -# doc_open = true -# if editor_help.name != text: -# text = editor_help.name -# script_list.set_item_tooltip(i, text + " Class Reference") - -# if script_list.get_selected_items()[0] == i and text in class_docs: -# custom_doc_open = true -# set_current_label(editor_help.get_child(0)) +# if not tree: +# _enter_tree() +# if not tree: +# return + +# doc_generator._update() + +# # Update search help tree items +# if tree.get_root(): +# search_flags = search_controls.get_child(3).get_item_id(search_controls.get_child(3).selected) +# search_flags |= SEARCH_CASE * int(search_controls.get_child(1).pressed) +# search_flags |= SEARCH_TREE * int(search_controls.get_child(2).pressed) +# search_term = search_controls.get_child(0).text + +# for name in class_docs: +# if fits_search(name, ITEM_CLASS): +# process_custom_item(name, ITEM_CLASS) + +# for method in class_docs[name].methods: +# if fits_search(method.name, ITEM_METHOD): +# process_custom_item(name + "." + method.name, ITEM_METHOD) + +# for _signal in class_docs[name].signals: +# if fits_search(_signal.name, ITEM_SIGNAL): +# process_custom_item(name + "." + _signal.name, ITEM_SIGNAL) + +# for constant in class_docs[name].constants: +# if fits_search(constant.name, ITEM_CONSTANT): +# process_custom_item(name + "." + constant.name, ITEM_CONSTANT) + +# for property in class_docs[name].properties: +# if fits_search(property.name, ITEM_PROPERTY): +# process_custom_item(name + "." + property.name, ITEM_PROPERTY) + +# var custom_doc_open := false +# var doc_open := false +# for i in script_list.get_item_count(): +# var icon := script_list.get_item_icon(i) +# var text := script_list.get_item_text(i) + +# var editor_help = script_tabs.get_child(script_list.get_item_metadata(i)) +# if icon == theme.get_icon("Help", "EditorIcons"): +# if script_list.get_selected_items()[0] == i: +# doc_open = true +# if editor_help.name != text: +# text = editor_help.name +# script_list.set_item_tooltip(i, text + " Class Reference") + +# if script_list.get_selected_items()[0] == i and text in class_docs: +# custom_doc_open = true +# set_current_label(editor_help.get_child(0)) # # else: # # set_current_label(null) -# script_list.call_deferred("set_item_text", i, text) +# script_list.call_deferred("set_item_text", i, text) -# if custom_doc_open: -# section_list.get_parent().get_child(3).set_deferred("visible", false) -# section_list.visible = true -# else: -# section_list.get_parent().get_child(3).set_deferred("visible", doc_open) -# section_list.visible = false +# if custom_doc_open: +# section_list.get_parent().get_child(3).set_deferred("visible", false) +# section_list.visible = true +# else: +# section_list.get_parent().get_child(3).set_deferred("visible", doc_open) +# section_list.visible = false # func get_parent_class(_class: String) -> String: -# if class_docs.has(_class): -# return class_docs[_class].base -# return ClassDB.get_parent_class(_class) +# if class_docs.has(_class): +# return class_docs[_class].base +# return ClassDB.get_parent_class(_class) # func fits_search(name: String, type: int) -> bool: -# if type == ITEM_CLASS and not (search_flags & SEARCH_CLASS): -# return false -# elif type == ITEM_METHOD and not (search_flags & SEARCH_METHOD) or search_term.empty(): -# return false -# elif type == ITEM_SIGNAL and not (search_flags & SEARCH_SIGNAL) or search_term.empty(): -# return false -# elif type == ITEM_CONSTANT and not (search_flags & SEARCH_CONSTANT) or search_term.empty(): -# return false -# elif type == ITEM_PROPERTY and not (search_flags & SEARCH_PROPERTY) or search_term.empty(): -# return false - -# if not search_term.empty(): -# if (search_flags & SEARCH_CASE) and name.find(search_term) == -1: -# return false -# elif ~(search_flags & SEARCH_CASE) and name.findn(search_term) == -1: -# return false - -# return true +# if type == ITEM_CLASS and not (search_flags & SEARCH_CLASS): +# return false +# elif type == ITEM_METHOD and not (search_flags & SEARCH_METHOD) or search_term.empty(): +# return false +# elif type == ITEM_SIGNAL and not (search_flags & SEARCH_SIGNAL) or search_term.empty(): +# return false +# elif type == ITEM_CONSTANT and not (search_flags & SEARCH_CONSTANT) or search_term.empty(): +# return false +# elif type == ITEM_PROPERTY and not (search_flags & SEARCH_PROPERTY) or search_term.empty(): +# return false + +# if not search_term.empty(): +# if (search_flags & SEARCH_CASE) and name.find(search_term) == -1: +# return false +# elif ~(search_flags & SEARCH_CASE) and name.findn(search_term) == -1: +# return false + +# return true # func update_doc(label: RichTextLabel) -> void: -# doc_exporter.label = label -# doc_exporter._generate(class_docs[label.get_parent().name]) +# doc_exporter.label = label +# doc_exporter._generate(class_docs[label.get_parent().name]) -# var section_lines := doc_exporter.section_lines -# section_list.clear() -# for i in len(section_lines): -# section_list.add_item(section_lines[i][0]) -# section_list.set_item_metadata(i, section_lines[i][1]) +# var section_lines := doc_exporter.section_lines +# section_list.clear() +# for i in len(section_lines): +# section_list.add_item(section_lines[i][0]) +# section_list.set_item_metadata(i, section_lines[i][1]) # func process_custom_item(name: String, type := ITEM_CLASS) -> TreeItem: -# # Create tree item if it's not their. -# if weakref(doc_items.get(name + str(type))).get_ref(): -# doc_items[name + str(type)].clear_custom_color(0) -# doc_items[name + str(type)].clear_custom_color(1) -# return doc_items[name + str(type)] - -# var parent := tree.get_root() -# var sub_name: String -# if name.find(".") != -1: -# var split := name.split(".") -# name = split[0] -# sub_name = split[1] - -# var doc: DocItem = class_docs[name] - -# if search_flags & SEARCH_TREE: -# # Get inheritance chain of the class. -# var inherit_chain = [doc.base] -# while not inherit_chain[-1].empty(): -# inherit_chain.append(get_parent_class(inherit_chain[-1])) -# inherit_chain.pop_back() -# inherit_chain.invert() -# if not sub_name.empty(): -# inherit_chain.append(name) - -# # Find the tree item the class should be under. -# for inherit in inherit_chain: -# var failed := true -# var child := parent.get_children() -# while child and child.get_parent() == parent: -# if child.get_text(0) == inherit: -# parent = child -# failed = false -# break -# child = child.get_next() - -# if failed: -# var new_parent: TreeItem -# if inherit in class_docs: -# new_parent = process_custom_item(inherit) -# if not new_parent: -# new_parent = tree.create_item(parent) -# new_parent.set_text(0, inherit) -# new_parent.set_text(1, "Class") -# new_parent.set_icon(0, get_class_icon(inherit)) -# new_parent.set_metadata(0, "class_name:" + inherit) -# new_parent.set_custom_color(0, disabled_color) -# new_parent.set_custom_color(1, disabled_color) -# parent = new_parent - -# var item := tree.create_item(parent) -# if not sub_name.empty(): -# name += "." + sub_name -# var display_name := sub_name if search_flags & SEARCH_TREE else name -# match type: -# ITEM_CLASS: -# item.set_text(0, name) -# item.set_text(1, "Class") -# item.set_tooltip(0, doc.brief) -# item.set_tooltip(1, doc.brief) -# item.set_metadata(0, "class_name:" + name) -# item.set_icon(0, get_class_icon("Object")) - -# ITEM_METHOD: -# doc = doc.get_method_doc(sub_name) -# item.set_text(0, display_name) -# item.set_text(1, "Method") -# item.set_tooltip(0, doc.return_type + " " + name + "()") -# item.set_tooltip(1, item.get_tooltip(0)) -# item.set_metadata(0, "class_method:" + name.replace(".", ":")) -# item.set_icon(0, theme.get_icon("MemberMethod", "EditorIcons")) - -# ITEM_SIGNAL: -# doc = doc.get_signal_doc(sub_name) -# item.set_text(0, display_name) -# item.set_text(1, "Signal") -# item.set_tooltip(0, name + "()") -# item.set_tooltip(1, item.get_tooltip(0)) -# item.set_metadata(0, "class_signal:" + name.replace(".", ":")) -# item.set_icon(0, theme.get_icon("MemberSignal", "EditorIcons")) - -# ITEM_CONSTANT: -# doc = doc.get_constant_doc(sub_name) -# item.set_text(0, display_name) -# item.set_text(1, "Constant") -# item.set_tooltip(0, name) -# item.set_tooltip(1, item.get_tooltip(0)) -# item.set_metadata(0, "class_constant:" + name.replace(".", ":")) -# item.set_icon(0, theme.get_icon("MemberConstant", "EditorIcons")) - -# ITEM_PROPERTY: -# doc = doc.get_property_doc(sub_name) -# item.set_text(0, display_name) -# item.set_text(1, "Property") -# item.set_tooltip(0, doc.type + " " + name) -# item.set_tooltip(1, item.get_tooltip(0)) -# item.set_metadata(0, "class_property:" + name.replace(".", ":")) -# item.set_icon(0, theme.get_icon("MemberProperty", "EditorIcons")) - -# var tooltip = item.get_tooltip(0) -# for key in doc.meta: -# tooltip += "\n" + snakekebab2pascal(key) + ": " + doc.meta[key] -# item.set_tooltip(0, tooltip) - -# doc_items[name + str(type)] = item -# return item +# # Create tree item if it's not their. +# if weakref(doc_items.get(name + str(type))).get_ref(): +# doc_items[name + str(type)].clear_custom_color(0) +# doc_items[name + str(type)].clear_custom_color(1) +# return doc_items[name + str(type)] + +# var parent := tree.get_root() +# var sub_name: String +# if name.find(".") != -1: +# var split := name.split(".") +# name = split[0] +# sub_name = split[1] + +# var doc: DocItem = class_docs[name] + +# if search_flags & SEARCH_TREE: +# # Get inheritance chain of the class. +# var inherit_chain = [doc.base] +# while not inherit_chain[-1].empty(): +# inherit_chain.append(get_parent_class(inherit_chain[-1])) +# inherit_chain.pop_back() +# inherit_chain.invert() +# if not sub_name.empty(): +# inherit_chain.append(name) + +# # Find the tree item the class should be under. +# for inherit in inherit_chain: +# var failed := true +# var child := parent.get_children() +# while child and child.get_parent() == parent: +# if child.get_text(0) == inherit: +# parent = child +# failed = false +# break +# child = child.get_next() + +# if failed: +# var new_parent: TreeItem +# if inherit in class_docs: +# new_parent = process_custom_item(inherit) +# if not new_parent: +# new_parent = tree.create_item(parent) +# new_parent.set_text(0, inherit) +# new_parent.set_text(1, "Class") +# new_parent.set_icon(0, get_class_icon(inherit)) +# new_parent.set_metadata(0, "class_name:" + inherit) +# new_parent.set_custom_color(0, disabled_color) +# new_parent.set_custom_color(1, disabled_color) +# parent = new_parent + +# var item := tree.create_item(parent) +# if not sub_name.empty(): +# name += "." + sub_name +# var display_name := sub_name if search_flags & SEARCH_TREE else name +# match type: +# ITEM_CLASS: +# item.set_text(0, name) +# item.set_text(1, "Class") +# item.set_tooltip(0, doc.brief) +# item.set_tooltip(1, doc.brief) +# item.set_metadata(0, "class_name:" + name) +# item.set_icon(0, get_class_icon("Object")) + +# ITEM_METHOD: +# doc = doc.get_method_doc(sub_name) +# item.set_text(0, display_name) +# item.set_text(1, "Method") +# item.set_tooltip(0, doc.return_type + " " + name + "()") +# item.set_tooltip(1, item.get_tooltip(0)) +# item.set_metadata(0, "class_method:" + name.replace(".", ":")) +# item.set_icon(0, theme.get_icon("MemberMethod", "EditorIcons")) + +# ITEM_SIGNAL: +# doc = doc.get_signal_doc(sub_name) +# item.set_text(0, display_name) +# item.set_text(1, "Signal") +# item.set_tooltip(0, name + "()") +# item.set_tooltip(1, item.get_tooltip(0)) +# item.set_metadata(0, "class_signal:" + name.replace(".", ":")) +# item.set_icon(0, theme.get_icon("MemberSignal", "EditorIcons")) + +# ITEM_CONSTANT: +# doc = doc.get_constant_doc(sub_name) +# item.set_text(0, display_name) +# item.set_text(1, "Constant") +# item.set_tooltip(0, name) +# item.set_tooltip(1, item.get_tooltip(0)) +# item.set_metadata(0, "class_constant:" + name.replace(".", ":")) +# item.set_icon(0, theme.get_icon("MemberConstant", "EditorIcons")) + +# ITEM_PROPERTY: +# doc = doc.get_property_doc(sub_name) +# item.set_text(0, display_name) +# item.set_text(1, "Property") +# item.set_tooltip(0, doc.type + " " + name) +# item.set_tooltip(1, item.get_tooltip(0)) +# item.set_metadata(0, "class_property:" + name.replace(".", ":")) +# item.set_icon(0, theme.get_icon("MemberProperty", "EditorIcons")) + +# var tooltip = item.get_tooltip(0) +# for key in doc.meta: +# tooltip += "\n" + snakekebab2pascal(key) + ": " + doc.meta[key] +# item.set_tooltip(0, tooltip) + +# doc_items[name + str(type)] = item +# return item # func snakekebab2pascal(string: String) -> String: -# var result := PoolStringArray() -# var prev_is_underscore := true # Make false for camelCase -# for ch in string: -# if ch == "_" or ch == "-": -# prev_is_underscore = true -# else: -# if prev_is_underscore: -# result.append(ch.to_upper()) -# else: -# result.append(ch) -# prev_is_underscore = false +# var result := PoolStringArray() +# var prev_is_underscore := true # Make false for camelCase +# for ch in string: +# if ch == "_" or ch == "-": +# prev_is_underscore = true +# else: +# if prev_is_underscore: +# result.append(ch.to_upper()) +# else: +# result.append(ch) +# prev_is_underscore = false -# return result.join("") +# return result.join("") # func purge_duplicate_tabs() -> void: -# var selected_duplicate := "" -# var i := 0 -# while i < script_list.get_item_count(): -# if script_list.get_item_icon(i) != theme.get_icon("Help", "EditorIcons"): -# i += 1 -# continue - -# var text := script_tabs.get_child(script_list.get_item_metadata(i)).name -# # Possible duplicate -# var is_duplicate := false -# if text[-1].is_valid_integer(): -# for doc in class_docs: -# if text.find(doc) != -1 and text.right(len(doc)).is_valid_integer(): -# text = doc -# is_duplicate = true -# break - -# if is_duplicate: -# # HACK: Creating a couple input events to simulate deleting the duplicate tab -# if script_list.is_visible_in_tree(): -# var prev_count := script_list.get_item_count() - -# if script_list.is_selected(i): -# selected_duplicate = text - -# script_list.select(i) -# var event := InputEventKey.new() -# event.scancode = KEY_W -# event.control = true -# event.pressed = true -# get_tree().input_event(event) -# event = event.duplicate() -# event.pressed = false -# get_tree().input_event(event) - -# # Makes sure that we don't run into an infinite loop. -# i -= prev_count - script_list.get_item_count() -# i += 1 - -# if not selected_duplicate.empty(): -# for j in script_list.get_item_count(): -# var editor_help := script_tabs.get_child(script_list.get_item_metadata(j)) -# if editor_help.name == selected_duplicate: -# script_list.select(j) -# script_list.emit_signal("item_selected", j) -# set_current_label(editor_help.get_child(0)) -# break +# var selected_duplicate := "" +# var i := 0 +# while i < script_list.get_item_count(): +# if script_list.get_item_icon(i) != theme.get_icon("Help", "EditorIcons"): +# i += 1 +# continue + +# var text := script_tabs.get_child(script_list.get_item_metadata(i)).name +# # Possible duplicate +# var is_duplicate := false +# if text[-1].is_valid_integer(): +# for doc in class_docs: +# if text.find(doc) != -1 and text.right(len(doc)).is_valid_integer(): +# text = doc +# is_duplicate = true +# break + +# if is_duplicate: +# # HACK: Creating a couple input events to simulate deleting the duplicate tab +# if script_list.is_visible_in_tree(): +# var prev_count := script_list.get_item_count() + +# if script_list.is_selected(i): +# selected_duplicate = text + +# script_list.select(i) +# var event := InputEventKey.new() +# event.scancode = KEY_W +# event.control = true +# event.pressed = true +# get_tree().input_event(event) +# event = event.duplicate() +# event.pressed = false +# get_tree().input_event(event) + +# # Makes sure that we don't run into an infinite loop. +# i -= prev_count - script_list.get_item_count() +# i += 1 + +# if not selected_duplicate.empty(): +# for j in script_list.get_item_count(): +# var editor_help := script_tabs.get_child(script_list.get_item_metadata(j)) +# if editor_help.name == selected_duplicate: +# script_list.select(j) +# script_list.emit_signal("item_selected", j) +# set_current_label(editor_help.get_child(0)) +# break # func set_current_label(label: RichTextLabel) -> void: -# if current_label != label: -# if is_instance_valid(current_label): -# current_label.disconnect("meta_clicked", self, "_on_EditorHelpLabel_meta_clicked") +# if current_label != label: +# if is_instance_valid(current_label): +# current_label.disconnect("meta_clicked", self, "_on_EditorHelpLabel_meta_clicked") -# if is_instance_valid(label): -# update_doc(label) -# current_label = label -# current_label.connect("meta_clicked", self, "_on_EditorHelpLabel_meta_clicked", [current_label], CONNECT_DEFERRED) +# if is_instance_valid(label): +# update_doc(label) +# current_label = label +# current_label.connect("meta_clicked", self, "_on_EditorHelpLabel_meta_clicked", [current_label], CONNECT_DEFERRED) # func get_class_icon(_class: String) -> Texture: -# if theme.has_icon(_class, "EditorIcons"): -# return theme.get_icon(_class, "EditorIcons") -# elif _class in class_docs: -# var path: String = class_docs[_class].icon -# if not path.empty() and load(path) is Texture: -# return load(path) as Texture -# return get_class_icon("Object") +# if theme.has_icon(_class, "EditorIcons"): +# return theme.get_icon(_class, "EditorIcons") +# elif _class in class_docs: +# var path: String = class_docs[_class].icon +# if not path.empty() and load(path) is Texture: +# return load(path) as Texture +# return get_class_icon("Object") # func get_child_chain(node: Node, indices: Array) -> Node: -# var child := node -# for index in indices: -# child = child.get_child(index) -# if not child: -# return null -# return child +# var child := node +# for index in indices: +# child = child.get_child(index) +# if not child: +# return null +# return child # func find_node_by_class(node: Node, _class: String) -> Node: -# if node.is_class(_class): -# return node +# if node.is_class(_class): +# return node -# for child in node.get_children(): -# var result = find_node_by_class(child, _class) -# if result: -# return result +# for child in node.get_children(): +# var result = find_node_by_class(child, _class) +# if result: +# return result -# return null +# return null # func _on_DocTimer_timeout() -> void: -# doc_exporter.plugin = self -# doc_exporter.theme = theme -# doc_exporter.editor_settings = get_editor_interface().get_editor_settings() -# doc_exporter.class_docs = class_docs -# doc_exporter.update_theme_vars() -# doc_generator.plugin = self - -# var classes: Array = ProjectSettings.get("_global_script_classes") -# var class_icons: Dictionary = ProjectSettings.get("_global_script_class_icons") - -# # Include autoloads -# var file := File.new() -# while not file.open("res://project.godot", File.READ): -# var project_string := file.get_as_text() -# var autoload_loc := project_string.find("[autoload]\n") -# if autoload_loc == -1: -# break -# autoload_loc += len("[autoload]\n\n") - -# var list := project_string.right(autoload_loc).split("\n") -# for i in len(list): -# var line := list[i] -# if line.empty(): -# continue -# if line.begins_with("["): -# break - -# var entry := line.split("=") -# # An asterisk indicates that the singleton's enabled. -# if entry[1][1] != "*": -# continue - -# # Only gdscript and scenes are supported. -# var type := "other" -# var base := "" -# var path := entry[1].trim_prefix("\"*").trim_suffix("\"") -# var script: GDScript - -# if path.ends_with(".tscn") or path.ends_with(".scn"): -# type = "scene" -# elif type.ends_with(".gd"): -# type = "script" - -# if type == "other": -# continue -# elif type == "scene": -# script = load(path).instance().get_script() -# else: -# script = load(path) - -# if not script: -# continue -# if script.resource_path.empty(): -# continue -# else: -# path = script.resource_path -# base = script.get_instance_base_type() - -# classes.append({ -# "base": base, -# "class": entry[0], -# "language": "GDScript" if path.find(".gd") != -1 else "Other", -# "path": path, -# "is_autoload": true -# }) - -# break -# file.close() - -# var docs := {} -# for _class in classes: -# if _class["language"] != "GDScript": -# continue - -# # TODO: Add file path to class document item -# var doc := doc_generator.generate(_class["class"], _class["base"], _class["path"]) -# if not doc: -# continue - -# doc.icon = class_icons.get(doc.name, "") -# doc.is_singleton = _class.has("is_autoload") -# docs[doc.name] = doc -# class_docs[doc.name] = doc -# if not doc.name in doc_exporter.class_list: -# doc_exporter.class_list.append(doc.name) - -# # Periodically clean up tree items -# for name in doc_items: -# if not doc_items[name]: -# doc_items.erase(name) - -# for _class in class_docs: -# if not docs.has(_class): -# doc_exporter.class_list.erase(_class) -# class_docs.erase(_class) +# doc_exporter.plugin = self +# doc_exporter.theme = theme +# doc_exporter.editor_settings = get_editor_interface().get_editor_settings() +# doc_exporter.class_docs = class_docs +# doc_exporter.update_theme_vars() +# doc_generator.plugin = self + +# var classes: Array = ProjectSettings.get("_global_script_classes") +# var class_icons: Dictionary = ProjectSettings.get("_global_script_class_icons") + +# # Include autoloads +# var file := File.new() +# while not file.open("res://project.godot", File.READ): +# var project_string := file.get_as_text() +# var autoload_loc := project_string.find("[autoload]\n") +# if autoload_loc == -1: +# break +# autoload_loc += len("[autoload]\n\n") + +# var list := project_string.right(autoload_loc).split("\n") +# for i in len(list): +# var line := list[i] +# if line.empty(): +# continue +# if line.begins_with("["): +# break + +# var entry := line.split("=") +# # An asterisk indicates that the singleton's enabled. +# if entry[1][1] != "*": +# continue + +# # Only gdscript and scenes are supported. +# var type := "other" +# var base := "" +# var path := entry[1].trim_prefix("\"*").trim_suffix("\"") +# var script: GDScript + +# if path.ends_with(".tscn") or path.ends_with(".scn"): +# type = "scene" +# elif type.ends_with(".gd"): +# type = "script" + +# if type == "other": +# continue +# elif type == "scene": +# script = load(path).instance().get_script() +# else: +# script = load(path) + +# if not script: +# continue +# if script.resource_path.empty(): +# continue +# else: +# path = script.resource_path +# base = script.get_instance_base_type() + +# classes.append({ +# "base": base, +# "class": entry[0], +# "language": "GDScript" if path.find(".gd") != -1 else "Other", +# "path": path, +# "is_autoload": true +# }) + +# break +# file.close() + +# var docs := {} +# for _class in classes: +# if _class["language"] != "GDScript": +# continue + +# # TODO: Add file path to class document item +# var doc := doc_generator.generate(_class["class"], _class["base"], _class["path"]) +# if not doc: +# continue + +# doc.icon = class_icons.get(doc.name, "") +# doc.is_singleton = _class.has("is_autoload") +# docs[doc.name] = doc +# class_docs[doc.name] = doc +# if not doc.name in doc_exporter.class_list: +# doc_exporter.class_list.append(doc.name) + +# # Periodically clean up tree items +# for name in doc_items: +# if not doc_items[name]: +# doc_items.erase(name) + +# for _class in class_docs: +# if not docs.has(_class): +# doc_exporter.class_list.erase(_class) +# class_docs.erase(_class) # func _on_EditorHelpLabel_meta_clicked(meta: String, label: RichTextLabel) -> void: -# if meta.begins_with("$"): -# var select := meta.substr(1, len(meta)) -# var _class_name := "" -# if select.find(".") != -1: -# _class_name = select.split(".")[0] -# else: -# _class_name = "@GlobalScope" -# search_help.emit_signal("go_to_help", "class_enum:" + _class_name + ":" + select) -# elif meta.begins_with("#"): -# search_help.emit_signal("go_to_help", "class_name:" + meta.substr(1, len(meta))) -# elif meta.begins_with("@"): -# var tag_end := meta.find(" ") -# var tag := meta.substr(1, tag_end - 1) -# var link := meta.substr(tag_end + 1, meta.length()).lstrip(" ") - -# var topic := "" -# var table: Dictionary - -# if tag == "method": -# topic = "class_method" -# table = doc_exporter.method_line -# elif tag == "member": -# topic = "class_property" -# table = doc_exporter.property_line -# elif tag == "enum": -# topic = "class_enum" -# table = doc_exporter.enum_line -# elif tag == "signal": -# topic = "class_signal" -# table = doc_exporter.signal_line -# elif tag == "constant": -# topic = "class_constant" -# table = doc_exporter.constant_line -# else: -# return - -# if link.find(".") != -1: -# search_help.emit_signal("go_to_help", topic + ":" + link.split(".")[0] + ":" + link.split(".")[1]) -# else: -# if table.has(link): -# # Found in the current page -# current_label.scroll_to_line(table[link]) -# else: -# pass # Oh well. +# if meta.begins_with("$"): +# var select := meta.substr(1, len(meta)) +# var _class_name := "" +# if select.find(".") != -1: +# _class_name = select.split(".")[0] +# else: +# _class_name = "@GlobalScope" +# search_help.emit_signal("go_to_help", "class_enum:" + _class_name + ":" + select) +# elif meta.begins_with("#"): +# search_help.emit_signal("go_to_help", "class_name:" + meta.substr(1, len(meta))) +# elif meta.begins_with("@"): +# var tag_end := meta.find(" ") +# var tag := meta.substr(1, tag_end - 1) +# var link := meta.substr(tag_end + 1, meta.length()).lstrip(" ") + +# var topic := "" +# var table: Dictionary + +# if tag == "method": +# topic = "class_method" +# table = doc_exporter.method_line +# elif tag == "member": +# topic = "class_property" +# table = doc_exporter.property_line +# elif tag == "enum": +# topic = "class_enum" +# table = doc_exporter.enum_line +# elif tag == "signal": +# topic = "class_signal" +# table = doc_exporter.signal_line +# elif tag == "constant": +# topic = "class_constant" +# table = doc_exporter.constant_line +# else: +# return + +# if link.find(".") != -1: +# search_help.emit_signal("go_to_help", topic + ":" + link.split(".")[0] + ":" + link.split(".")[1]) +# else: +# if table.has(link): +# # Found in the current page +# current_label.scroll_to_line(table[link]) +# else: +# pass # Oh well. # func _on_SearchHelp_go_to_help(tag: String) -> void: -# purge_duplicate_tabs() -# var editor_help := script_tabs.get_child(script_list.get_selected_items()[0]) -# if editor_help.name in class_docs.keys(): -# set_current_label(editor_help.get_child(0)) - -# var what := tag.split(":")[0] -# var clss := tag.split(":")[1] -# var name := "" -# if len(tag.split(":")) == 3: -# name = tag.split(":")[2] - -# var de := doc_exporter -# var line := 0 -# if what == "class_desc": -# line = de.description_line -# elif what == "class_signal": -# if de.signal_line.has(name): -# line = de.signal_line[name] -# elif what == "class_method" or what == "class_method_desc": -# if de.method_line.has(name): -# line = de.method_line[name] -# elif what == "class_property": -# if de.property_line.has(name): -# line = de.property_line[name] -# elif what == "class_enum": -# if de.enum_line.has(name): -# line = de.enum_line[name] +# purge_duplicate_tabs() +# var editor_help := script_tabs.get_child(script_list.get_selected_items()[0]) +# if editor_help.name in class_docs.keys(): +# set_current_label(editor_help.get_child(0)) + +# var what := tag.split(":")[0] +# var clss := tag.split(":")[1] +# var name := "" +# if len(tag.split(":")) == 3: +# name = tag.split(":")[2] + +# var de := doc_exporter +# var line := 0 +# if what == "class_desc": +# line = de.description_line +# elif what == "class_signal": +# if de.signal_line.has(name): +# line = de.signal_line[name] +# elif what == "class_method" or what == "class_method_desc": +# if de.method_line.has(name): +# line = de.method_line[name] +# elif what == "class_property": +# if de.property_line.has(name): +# line = de.property_line[name] +# elif what == "class_enum": +# if de.enum_line.has(name): +# line = de.enum_line[name] # # elif what == "class_theme_item": # # if (theme_property_line.has(name)) # # line = theme_property_line[name] -# elif what == "class_constant": -# if de.constant_line.has(name): -# line = de.constant_line[name] -# elif what == "class_name": -# pass -# else: -# printerr("Could not go to help: " + tag) +# elif what == "class_constant": +# if de.constant_line.has(name): +# line = de.constant_line[name] +# elif what == "class_name": +# pass +# else: +# printerr("Could not go to help: " + tag) -# current_label.call_deferred("scroll_to_line", line) +# current_label.call_deferred("scroll_to_line", line) # func _on_SectionList_item_selected(index: int) -> void: -# if not current_label: -# return -# current_label.scroll_to_line(section_list.get_item_metadata(index)) +# if not current_label: +# return +# current_label.scroll_to_line(section_list.get_item_metadata(index)) diff --git a/test/TestUtils.gd b/test/TestUtils.gd index 1189f0b..4f33847 100644 --- a/test/TestUtils.gd +++ b/test/TestUtils.gd @@ -1,9 +1,8 @@ class_name TestUtils extends Object -static func instantiate(clazz: Script) -> Node: - var o = Node.new() - - o.set_script(clazz) +static func instantiate(script: Script) -> Node: + var o = Node.new() + o.set_script(script) return o diff --git a/test/firestore_test.gd b/test/firestore_test.gd index 2e22a48..98a00b9 100644 --- a/test/firestore_test.gd +++ b/test/firestore_test.gd @@ -4,28 +4,28 @@ export var email := "" export var password := "" func _ready() -> void: - Firebase.Auth.login_with_email_and_password(email, password) - yield(Firebase.Auth, "login_succeeded") - print("Logged in!") + Firebase.Auth.login_with_email_and_password(email, password) + yield(Firebase.Auth, "login_succeeded") + print("Logged in!") - var task: FirestoreTask + var task: FirestoreTask - Firebase.Firestore.disable_networking() + Firebase.Firestore.disable_networking() - task = Firebase.Firestore.list("test_collection", 5, "", "number") - print(yield(task, "listed_documents")) + task = Firebase.Firestore.list("test_collection", 5, "", "number") + print(yield(task, "listed_documents")) - var test : FirestoreCollection = Firebase.Firestore.collection("test_collection") + var test : FirestoreCollection = Firebase.Firestore.collection("test_collection") - for i in 5: - var name = "some_document_%d" % hash(str(i)) - task = test.delete(name) - task = test.update(name, {"number": i + 10}) + for i in 5: + var name = "some_document_%d" % hash(str(i)) + task = test.delete(name) + task = test.update(name, {"number": i + 10}) - var document = yield(task, "task_finished") + var document = yield(task, "task_finished") - Firebase.Firestore.enable_networking() + Firebase.Firestore.enable_networking() - task = test.get("some_document_%d" % hash(str(4))) - document = yield(task, "task_finished") - print(document) + task = test.get("some_document_%d" % hash(str(4))) + document = yield(task, "task_finished") + print(document) diff --git a/test/storage_stress_test.gd b/test/storage_stress_test.gd index 0237a85..1eaff71 100644 --- a/test/storage_stress_test.gd +++ b/test/storage_stress_test.gd @@ -1,52 +1,54 @@ extends Node2D + var offset := 0 export var email := "" export var password := "" + func _ready() -> void: - Firebase.Auth.login_with_email_and_password(email, password) - yield(Firebase.Auth, "login_succeeded") - print("Logged in!") + Firebase.Auth.login_with_email_and_password(email, password) + yield(Firebase.Auth, "login_succeeded") + print("Logged in!") - var ref = Firebase.Storage.ref("test/test_image0.png") - var task = ref.put_file("res://icon.png") - task.connect("task_finished", self, "_on_task_finished", [task]) + var ref = Firebase.Storage.ref("test/test_image0.png") + var task = ref.put_file("res://icon.png") + task.connect("task_finished", self, "_on_task_finished", [task]) - for i in range(10): - task = ref.get_data() - task.connect("task_finished", self, "_on_task_finished", [task]) + for i in range(10): + task = ref.get_data() + task.connect("task_finished", self, "_on_task_finished", [task]) - task = ref.delete() - task.connect("task_finished", self, "_on_task_finished", [task]) + task = ref.delete() + task.connect("task_finished", self, "_on_task_finished", [task]) func _on_task_finished(task: StorageTask) -> void: - if task.result or task.response_code >= 400: - if typeof(task.data) == TYPE_DICTIONARY: - printerr(task.data) - else: - printerr(JSON.parse(task.data.get_string_from_utf8()).result) - return - - match task.action: - StorageTask.Task.TASK_UPLOAD: - print("%s uploaded!" % task.ref) - - StorageTask.Task.TASK_DOWNLOAD: - var image := Image.new() - image.load_png_from_buffer(task.data) - var tex := ImageTexture.new() - tex.create_from_image(image) - - var sprite := Sprite.new() - sprite.scale *= 0.7 - sprite.centered = false - sprite.texture = tex - sprite.position.x = offset - add_child(sprite) - offset += 100 - - StorageTask.Task.TASK_DELETE: - print("%s deleted!" % task.ref) + if task.result or task.response_code >= 400: + if typeof(task.data) == TYPE_DICTIONARY: + printerr(task.data) + else: + printerr(JSON.parse(task.data.get_string_from_utf8()).result) + return + + match task.action: + StorageTask.Task.TASK_UPLOAD: + print("%s uploaded!" % task.ref) + + StorageTask.Task.TASK_DOWNLOAD: + var image := Image.new() + image.load_png_from_buffer(task.data) + var tex := ImageTexture.new() + tex.create_from_image(image) + + var sprite := Sprite.new() + sprite.scale *= 0.7 + sprite.centered = false + sprite.texture = tex + sprite.position.x = offset + add_child(sprite) + offset += 100 + + StorageTask.Task.TASK_DELETE: + print("%s deleted!" % task.ref) diff --git a/test/unit/test_FirebaseDatabaseStore.gd b/test/unit/test_FirebaseDatabaseStore.gd index 1104454..4ebee7f 100644 --- a/test/unit/test_FirebaseDatabaseStore.gd +++ b/test/unit/test_FirebaseDatabaseStore.gd @@ -1,208 +1,221 @@ extends "res://addons/gut/test.gd" + const FirebaseDatabaseStore = preload("res://addons/godot-firebase/database/database_store.gd") const TestKey = "-MPrgu_F8OXiL-VpRxjq" const TestObject = { - "I": "Some Value", - "II": "Some Other Value", - "III": [111, 222, 333, 444, 555], - "IV": { - "a": "Another Value", - "b": "Yet Another Value" - } + "I": "Some Value", + "II": "Some Other Value", + "III": [111, 222, 333, 444, 555], + "IV": { + "a": "Another Value", + "b": "Yet Another Value" + } } const TestObjectOther = { - "a": "A Different Value", - "b": "Another One", - "c": "A New Value" + "a": "A Different Value", + "b": "Another One", + "c": "A New Value" } const TestArray = [666, 777, 888, 999] const TestValue = 12345.6789 class TestPutOperations: - extends "res://addons/gut/test.gd" + extends "res://addons/gut/test.gd" + + + func test_put_object(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) + + store.put(TestKey, TestObject) + + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey] + + assert_eq_deep(store_object, TestObject) + + store.queue_free() - func test_put_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - store.put(TestKey, TestObject) + func test_put_nested_object(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] + store.put(TestKey, TestObject) + store.put(TestKey + "/V", TestObjectOther) - assert_eq_deep(store_object, TestObject) + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["V"] - store.queue_free() + assert_eq_deep(store_object, TestObjectOther) - func test_put_nested_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + store.queue_free() - store.put(TestKey, TestObject) - store.put(TestKey + "/V", TestObjectOther) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["V"] + func test_put_array_value(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - assert_eq_deep(store_object, TestObjectOther) + store.put(TestKey, TestObject) + store.put(TestKey + "/III", TestArray) - store.queue_free() + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["III"] - func test_put_array_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + assert_eq_deep(store_object, TestArray) - store.put(TestKey, TestObject) - store.put(TestKey + "/III", TestArray) + store.queue_free() - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["III"] - assert_eq_deep(store_object, TestArray) + func test_put_normal_value(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - store.queue_free() + store.put(TestKey, TestObject) + store.put(TestKey + "/II", TestValue) - func test_put_normal_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["II"] - store.put(TestKey, TestObject) - store.put(TestKey + "/II", TestValue) + assert_eq_deep(store_object, TestValue) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["II"] + store.queue_free() - assert_eq_deep(store_object, TestValue) - store.queue_free() + func test_put_deleted_value(): + # NOTE: Firebase Realtime Database sets values to null to indicate that they have been + # deleted. + var store = TestUtils.instantiate(FirebaseDatabaseStore) - func test_put_deleted_value(): - # NOTE: Firebase Realtime Database sets values to null to indicate that they have been - # deleted. + store.put(TestKey, TestObject) + store.put(TestKey + "/II", null) - var store = TestUtils.instantiate(FirebaseDatabaseStore) + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey] - store.put(TestKey, TestObject) - store.put(TestKey + "/II", null) + assert_false(store_object.has("II"), "The value should have been deleted, but was not.") - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] + store.queue_free() - assert_false(store_object.has("II"), "The value should have been deleted, but was not.") - store.queue_free() + func test_put_new_object(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - func test_put_new_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + store.put(TestKey, TestObject) - store.put(TestKey, TestObject) + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey] - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] + assert_eq_deep(store_object, TestObject) - assert_eq_deep(store_object, TestObject) + store.queue_free() - store.queue_free() - func test_put_new_nested_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + func test_put_new_nested_object(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - store.put(TestKey + "/V", TestObjectOther) + store.put(TestKey + "/V", TestObjectOther) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["V"] + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["V"] - assert_eq_deep(store_object, TestObjectOther) + assert_eq_deep(store_object, TestObjectOther) - store.queue_free() + store.queue_free() - func test_put_new_array_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) - store.put(TestKey + "/III", TestArray) + func test_put_new_array_value(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["III"] + store.put(TestKey + "/III", TestArray) - assert_eq_deep(store_object, TestArray) + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["III"] - store.queue_free() + assert_eq_deep(store_object, TestArray) - func test_put_new_normal_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + store.queue_free() - store.put(TestKey + "/II", TestValue) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["II"] + func test_put_new_normal_value(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - assert_eq_deep(store_object, TestValue) + store.put(TestKey + "/II", TestValue) - store.queue_free() + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["II"] + + assert_eq_deep(store_object, TestValue) + + store.queue_free() class TestPatchOperations: - extends "res://addons/gut/test.gd" + extends "res://addons/gut/test.gd" + + + func test_patch_object(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) + + store.patch(TestKey, TestObject) + + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey] - func test_patch_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + assert_eq_deep(store_object, TestObject) - store.patch(TestKey, TestObject) + store.queue_free() - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] - assert_eq_deep(store_object, TestObject) + func test_patch_nested_object(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - store.queue_free() + store.put(TestKey, TestObject) + store.patch(TestKey + "/V", TestObjectOther) - func test_patch_nested_object(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["V"] - store.put(TestKey, TestObject) - store.patch(TestKey + "/V", TestObjectOther) + assert_eq_deep(store_object, TestObjectOther) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["V"] + store.queue_free() - assert_eq_deep(store_object, TestObjectOther) - store.queue_free() + func test_patch_array_value(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - func test_patch_array_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + store.put(TestKey, TestObject) + store.patch(TestKey + "/III", TestArray) - store.put(TestKey, TestObject) - store.patch(TestKey + "/III", TestArray) + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["III"] - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["III"] + assert_eq_deep(store_object, TestArray) - assert_eq_deep(store_object, TestArray) + store.queue_free() - store.queue_free() - func test_patch_normal_value(): - var store = TestUtils.instantiate(FirebaseDatabaseStore) + func test_patch_normal_value(): + var store = TestUtils.instantiate(FirebaseDatabaseStore) - store.put(TestKey, TestObject) - store.patch(TestKey + "/II", TestValue) + store.put(TestKey, TestObject) + store.patch(TestKey + "/II", TestValue) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey]["II"] + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey]["II"] - assert_eq_deep(store_object, TestValue) + assert_eq_deep(store_object, TestValue) - store.queue_free() + store.queue_free() - func test_patch_deleted_value(): - # NOTE: Firebase Realtime Database sets values to null to indicate that they have been - # deleted. - var store = TestUtils.instantiate(FirebaseDatabaseStore) + func test_patch_deleted_value(): + # NOTE: Firebase Realtime Database sets values to null to indicate that they have been + # deleted. + var store = TestUtils.instantiate(FirebaseDatabaseStore) - store.put(TestKey, TestObject) - store.patch(TestKey + "/II", null) + store.put(TestKey, TestObject) + store.patch(TestKey + "/II", null) - var store_data: Dictionary = store.get_data() - var store_object = store_data[TestKey] + var store_data: Dictionary = store.get_data() + var store_object = store_data[TestKey] - assert_false(store_object.has("II"), "The value should have been deleted, but was not.") + assert_false(store_object.has("II"), "The value should have been deleted, but was not.") - store.queue_free() + store.queue_free() diff --git a/test/unit/test_FirestoreDocument.gd b/test/unit/test_FirestoreDocument.gd index 281bc71..d695283 100644 --- a/test/unit/test_FirestoreDocument.gd +++ b/test/unit/test_FirestoreDocument.gd @@ -1,81 +1,48 @@ extends "res://addons/gut/test.gd" + const FirestoreDocument = preload("res://addons/godot-firebase/firestore/firestore_document.gd") class TestDeserialization: - extends "res://addons/gut/test.gd" - - func test_deserialize_array_of_dicts(): - var doc_infos : Dictionary = { - "name":"projects/godot-firebase/databases/(default)/documents/rooms/EUZT", - "fields": { - "code": { - "stringValue":"EUZT" - }, - "players": { - "arrayValue": { - "values": [{ - "mapValue": { - "fields": { - "name": { - "stringValue":"Hello" - } - } - } - }, { - "mapValue":{ - "fields":{ - "name":{ - "stringValue":"Test" - } - } - } - }] - } - }, - }, - "createTime":"2021-02-16T07:24:11.106522Z", - "updateTime":"2021-02-16T08:21:32.131028Z" - } - var expected_doc_fields : Dictionary = { - "code": "EUZT", - "players": [ - { "name": "Hello" }, - { "name": "Test" }, - ] - } - var firestore_document : FirestoreDocument = FirestoreDocument.new(doc_infos) - - assert_eq_deep(firestore_document.doc_fields, expected_doc_fields) - - - - func test_deserialize_array_of_strings(): - - var doc_infos : Dictionary = { - "name":"projects/godot-firebase/databases/(default)/documents/rooms/EUZT", - "fields": { - "code": { - "stringValue":"EUZT" - }, - "things": { - "arrayValue": { - "values": [{ - "stringValue": "first" - }, { - "stringValue":"second" - }] - } - } - }, - "createTime":"2021-02-16T07:24:11.106522Z", - "updateTime":"2021-02-16T08:21:32.131028Z" - } - var expected_doc_fields : Dictionary = { - "code": "EUZT", - "things": ["first", "second"] - } - var firestore_document : FirestoreDocument = FirestoreDocument.new(doc_infos) - - assert_eq_deep(firestore_document.doc_fields, expected_doc_fields) - + extends "res://addons/gut/test.gd" + + + func test_deserialize_array_of_dicts(): + var doc_infos: Dictionary = { + "name": "projects/godot-firebase/databases/(default)/documents/rooms/EUZT", + "fields": { + "code": {"stringValue": "EUZT"}, + "players": { + "arrayValue": { + "values": [ + {"mapValue": {"fields": {"name": {"stringValue": "Hello"}}}}, + {"mapValue": {"fields": {"name": {"stringValue": "Test"}}}} + ] + } + } + }, + "createTime": "2021-02-16T07:24:11.106522Z", + "updateTime": "2021-02-16T08:21:32.131028Z" + } + var expected_doc_fields: Dictionary = { + "code": "EUZT", "players": [{"name": "Hello"}, {"name": "Test"}] + } + var firestore_document: FirestoreDocument = FirestoreDocument.new(doc_infos) + + assert_eq_deep(firestore_document.doc_fields, expected_doc_fields) + + + func test_deserialize_array_of_strings(): + var doc_infos: Dictionary = { + "name": "projects/godot-firebase/databases/(default)/documents/rooms/EUZT", + "fields": { + "code": {"stringValue": "EUZT"}, + "things": {"arrayValue": {"values": [{"stringValue": "first"}, {"stringValue": "second"}]}} + }, + "createTime": "2021-02-16T07:24:11.106522Z", + "updateTime": "2021-02-16T08:21:32.131028Z" + } + var expected_doc_fields: Dictionary = {"code": "EUZT", "things": ["first", "second"]} + var firestore_document: FirestoreDocument = FirestoreDocument.new(doc_infos) + + assert_eq_deep(firestore_document.doc_fields, expected_doc_fields)