├── addons └── FirebaseAPI │ ├── plugin.cfg │ ├── plugin.gd │ ├── realtime │ ├── realtime.gd │ ├── HTTPSSEClient.gd │ └── reference.gd │ ├── functions │ ├── function_task.gd │ └── functions.gd │ ├── auth │ ├── user_data.gd │ └── auth.gd │ ├── firebase.gd │ ├── firebase.tscn │ ├── storage │ ├── storage_task.gd │ ├── storage_reference.gd │ └── storage.gd │ ├── dynamiclinks │ └── dynamiclinks.gd │ └── firestore │ ├── firestore_task.gd │ ├── firestore_collection.gd │ ├── firestore_document.gd │ ├── firestore.gd │ └── firestore_query.gd ├── LICENSE └── README.md /addons/FirebaseAPI/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Firebase API" 4 | description="" 5 | author="Overvault" 6 | version="1.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | add_autoload_singleton("Firebase", "res://addons/FirebaseAPI/firebase.tscn") 7 | 8 | 9 | func _exit_tree(): 10 | remove_autoload_singleton("Firebase") 11 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/realtime/realtime.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseRealtime 3 | extends Node 4 | 5 | 6 | # This will create a reference in your realtime database. 7 | # This means that actions performed through this reference will START from there, 8 | # but you can always specify a more precise path and listen for changes or update the data. 9 | # If you DON'T specify a path (where allowed), actions will be performed at the reference root. 10 | # Calling this method without a path will create a reference of the entire database. 11 | # You can have more references at the same time. 12 | func get_realtime_reference(path := "", filter := {}) -> RealtimeReference: 13 | var firebase_reference := RealtimeReference.new() 14 | var pusher := HTTPRequest.new() 15 | var listener := HTTPSSEClient.new() 16 | var getter := HTTPRequest.new() 17 | firebase_reference.setup(path, filter, pusher, listener, getter) 18 | add_child(firebase_reference) 19 | return firebase_reference 20 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/functions/function_task.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FunctionTask 3 | extends RefCounted 4 | 5 | 6 | signal task_finished(result) 7 | signal function_executed(response, result) 8 | signal task_error(error) 9 | 10 | var data : Dictionary 11 | var error 12 | 13 | var _response_headers : PackedStringArray 14 | var _response_code : int = 0 15 | 16 | var _method := -1 17 | var _url : String 18 | var _fields : String 19 | var _headers : PackedStringArray 20 | 21 | 22 | func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 23 | var bod 24 | if JSON.parse_string(body.get_string_from_utf8()) != null: 25 | bod = JSON.parse_string(body.get_string_from_utf8()) 26 | else: 27 | bod = {content = body.get_string_from_utf8()} 28 | 29 | data = bod 30 | if response_code == HTTPClient.RESPONSE_OK and data != null: 31 | function_executed.emit(result, data) 32 | else: 33 | error = bod 34 | task_error.emit(bod) 35 | task_finished.emit(data) 36 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/auth/user_data.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseUserData 3 | extends RefCounted 4 | 5 | 6 | var local_id : String 7 | var email : String 8 | var email_verified := false 9 | var password_updated_at : float = 0 10 | var last_login_at : float = 0 11 | var created_at : float = 0 12 | var provider_user_info : Array 13 | 14 | var provider_id : String 15 | var display_name : String 16 | var photo_url : String 17 | 18 | 19 | func _init(p_userdata : Dictionary): 20 | local_id = p_userdata.get("localId", "") 21 | email = p_userdata.get("email", "") 22 | email_verified = p_userdata.get("emailVerified", false) 23 | last_login_at = float(p_userdata.get("lastLoginAt", 0)) 24 | created_at = float(p_userdata.get("createdAt", 0)) 25 | password_updated_at = float(p_userdata.get("passwordUpdatedAt", 0)) 26 | display_name = p_userdata.get("displayName", "") 27 | provider_user_info = p_userdata.get("providerUserInfo", []) 28 | if not provider_user_info.is_empty(): 29 | provider_id = provider_user_info[0].get("providerId", "") 30 | photo_url = provider_user_info[0].get("photoUrl", "") 31 | display_name = provider_user_info[0].get("displayName", "") 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Kyle Szklenski and contributors (Base code), 2023 Overvault 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/firebase.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | 5 | @onready var Auth : FirebaseAuth = $Auth 6 | @onready var Firestore : FirebaseFirestore = $Firestore 7 | @onready var Realtime : FirebaseRealtime = $Realtime 8 | @onready var Storage : FirebaseStorage = $Storage 9 | @onready var Functions : FirebaseFunctions = $Functions 10 | @onready var DynamicLinks : FirebaseDynamicLinks = $DynamicLinks 11 | 12 | var _config := { 13 | "apiKey" : "", 14 | "authDomain" : "", 15 | "databaseURL" : "", 16 | "projectId" : "", 17 | "storageBucket" : "", 18 | "messagingSenderId" : "", 19 | "appId" : "", 20 | "measurementId" : "", 21 | "clientId" : "", 22 | "clientSecret" : "", 23 | "domainUriPrefix" : "", 24 | "functionsGeoZone" : "", 25 | } 26 | 27 | 28 | # Call this method in your main scene's _ready() passing your Firebase config 29 | func setup_modules(config : Dictionary) -> void: 30 | for key in config: 31 | _config[key] = config[key] 32 | for module in get_children(): 33 | if module.has_method("_setup"): 34 | module._setup(_config) 35 | 36 | 37 | static func _printerr(error : String) -> void: 38 | printerr("[Firebase Error] >> " + error) 39 | 40 | 41 | static func _print(msg : String) -> void: 42 | print("[Firebase] >> " + msg) 43 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/firebase.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=3 uid="uid://crkd8dxfj14l7"] 2 | 3 | [ext_resource type="Script" path="res://addons/FirebaseAPI/firebase.gd" id="1_vvkam"] 4 | [ext_resource type="Script" path="res://addons/FirebaseAPI/auth/auth.gd" id="2_xquu1"] 5 | [ext_resource type="Script" path="res://addons/FirebaseAPI/realtime/realtime.gd" id="3_mg8hg"] 6 | [ext_resource type="Script" path="res://addons/FirebaseAPI/dynamiclinks/dynamiclinks.gd" id="4_i60eg"] 7 | [ext_resource type="Script" path="res://addons/FirebaseAPI/firestore/firestore.gd" id="5_ohygq"] 8 | [ext_resource type="Script" path="res://addons/FirebaseAPI/functions/functions.gd" id="6_nx5pw"] 9 | [ext_resource type="Script" path="res://addons/FirebaseAPI/storage/storage.gd" id="7_i6glr"] 10 | 11 | [node name="Firebase" type="Node"] 12 | script = ExtResource("1_vvkam") 13 | 14 | [node name="Auth" type="HTTPRequest" parent="."] 15 | script = ExtResource("2_xquu1") 16 | 17 | [node name="Realtime" type="Node" parent="."] 18 | script = ExtResource("3_mg8hg") 19 | 20 | [node name="DynamicLinks" type="Node" parent="."] 21 | script = ExtResource("4_i60eg") 22 | 23 | [node name="Firestore" type="Node" parent="."] 24 | script = ExtResource("5_ohygq") 25 | 26 | [node name="Functions" type="Node" parent="."] 27 | script = ExtResource("6_nx5pw") 28 | 29 | [node name="Storage" type="Node" parent="."] 30 | script = ExtResource("7_i6glr") 31 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/storage/storage_task.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseStorageTask 3 | extends RefCounted 4 | 5 | 6 | enum Task { 7 | TASK_UPLOAD, 8 | TASK_UPLOAD_META, 9 | TASK_DOWNLOAD, 10 | TASK_DOWNLOAD_META, 11 | TASK_DOWNLOAD_URL, 12 | TASK_LIST, 13 | TASK_LIST_ALL, 14 | TASK_DELETE, 15 | TASK_MAX 16 | } 17 | 18 | ## Emitted when the task is finished. Returns data depending on the success and action of the task. 19 | signal task_finished(data) 20 | 21 | var ref # Storage RefCounted (Can't static type due to cyclic reference) 22 | 23 | var action : int = -1 : set = set_action 24 | 25 | ## Data that the tracked task will/has returned. 26 | var data = PackedByteArray() # data can be of any type. 27 | 28 | ## The percentage of data that has been received. 29 | var progress := 0.0 30 | 31 | var result := -1 32 | 33 | var finished := false 34 | 35 | var response_headers : PackedStringArray 36 | 37 | ## The returned HTTP response code. 38 | var response_code : int = 0 39 | 40 | var _method := -1 41 | var _url : String 42 | var _headers : PackedStringArray 43 | 44 | 45 | func set_action(value : int) -> void: 46 | action = value 47 | match action: 48 | Task.TASK_UPLOAD: 49 | _method = HTTPClient.METHOD_POST 50 | Task.TASK_UPLOAD_META: 51 | _method = HTTPClient.METHOD_PATCH 52 | Task.TASK_DELETE: 53 | _method = HTTPClient.METHOD_DELETE 54 | _: 55 | _method = HTTPClient.METHOD_GET 56 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/dynamiclinks/dynamiclinks.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseDynamicLinks 3 | extends Node 4 | 5 | 6 | signal dynamic_link_generated(link_result) 7 | 8 | const _AUTHORIZATION_HEADER := "Authorization: Bearer " 9 | const _API_VERSION := "v1" 10 | 11 | var request := -1 12 | 13 | var _base_url : String 14 | 15 | var _request_list_node : HTTPRequest 16 | 17 | var _headers : PackedStringArray 18 | 19 | enum Requests { 20 | NONE = -1, 21 | GENERATE 22 | } 23 | 24 | 25 | func _setup(config_json : Dictionary) -> void: 26 | _base_url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=" + Firebase._config.apiKey 27 | _request_list_node = HTTPRequest.new() 28 | _request_list_node.request_completed.connect(_on_request_completed) 29 | add_child(_request_list_node) 30 | 31 | 32 | var _link_request_body : Dictionary = { 33 | "dynamicLinkInfo": { 34 | "domainUriPrefix": "", 35 | "link": "", 36 | "androidInfo": { 37 | "androidPackageName": "" 38 | }, 39 | "iosInfo": { 40 | "iosBundleId": "" 41 | } 42 | }, 43 | "suffix": { 44 | "option": "" 45 | } 46 | } 47 | 48 | ## This function is used to generate a dynamic link using the Firebase REST API 49 | ## It will return a JSON with the shortened link 50 | func generate_dynamic_link(long_link : String, APN : String, IBI : String, is_unguessable : bool) -> void: 51 | request = Requests.GENERATE 52 | _link_request_body.dynamicLinkInfo.domainUriPrefix = Firebase._config.domainUriPrefix 53 | _link_request_body.dynamicLinkInfo.link = long_link 54 | _link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN 55 | _link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI 56 | if is_unguessable: 57 | _link_request_body.suffix.option = "UNGUESSABLE" 58 | else: 59 | _link_request_body.suffix.option = "SHORT" 60 | _request_list_node.request(_base_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_link_request_body)) 61 | 62 | 63 | func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 64 | var result_body : Dictionary = JSON.parse_string(body.get_string_from_utf8()) 65 | dynamic_link_generated.emit(result_body.shortLink) 66 | request = Requests.NONE 67 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/realtime/HTTPSSEClient.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name HTTPSSEClient 3 | extends Node 4 | 5 | 6 | signal new_sse_event(headers, event, data) 7 | signal connected 8 | signal connection_error(error) 9 | 10 | var is_connected = false 11 | 12 | var httpclient := HTTPClient.new() 13 | var domain 14 | var url_after_domain 15 | var port 16 | var connection_in_progress = false 17 | var is_requested = false 18 | 19 | 20 | func set_coordinates(_domain : String, _url_after_domain : String, _port := -1) -> void: 21 | domain = _domain 22 | url_after_domain = _url_after_domain 23 | port = _port 24 | 25 | 26 | func attempt_to_connect() -> void: 27 | var err = httpclient.connect_to_host(domain, port) 28 | if err == OK: 29 | connected.emit() 30 | is_connected = true 31 | else: 32 | connection_error.emit(str(err)) 33 | 34 | 35 | func attempt_to_request(httpclient_status) -> void: 36 | if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING: 37 | return 38 | 39 | if httpclient_status == HTTPClient.STATUS_CONNECTED: 40 | var err = httpclient.request(HTTPClient.METHOD_POST, url_after_domain, ["Accept: text/event-stream"]) 41 | if err == OK: 42 | is_requested = true 43 | 44 | 45 | func _process(delta) -> void: 46 | if not is_connected: 47 | if not connection_in_progress: 48 | attempt_to_connect() 49 | connection_in_progress = true 50 | return 51 | httpclient.poll() 52 | var httpclient_status = httpclient.get_status() 53 | if not is_requested: 54 | attempt_to_request(httpclient_status) 55 | return 56 | 57 | if httpclient.has_response() or httpclient_status == HTTPClient.STATUS_BODY: 58 | var headers = httpclient.get_response_headers_as_dictionary() 59 | if httpclient_status == HTTPClient.STATUS_BODY: 60 | httpclient.poll() 61 | var chunk = httpclient.read_response_body_chunk() 62 | if chunk.size() == 0: 63 | return 64 | else: 65 | var body = chunk.get_string_from_utf8() 66 | if body != null: 67 | var event_data : Dictionary 68 | 69 | var event_idx = body.find("event:") 70 | if event_idx == -1: 71 | event_data.event = "continue_internal" 72 | else: 73 | var data_idx = body.find("data:", event_idx + "event:".length()) 74 | var event = body.substr(event_idx, data_idx) 75 | event = event.replace("event:", "").strip_edges() 76 | event_data.event = event 77 | 78 | var data_string = body.substr(data_idx + "data:".length()).strip_edges() 79 | var data = JSON.parse_string(data_string) 80 | if data != null: 81 | while data.path != "/": 82 | var segment = data.path.substr(data.path.rfind("/") + 1) 83 | var remaining = data.path.trim_suffix("/" + segment) 84 | if remaining == "": 85 | remaining = "/" 86 | data = {"path" : remaining, "data" : {segment : data.data}} 87 | event_data.data = data.data 88 | 89 | if not event_data.event in ["keep-alive", "continue_internal"]: 90 | if chunk.size() > 0: 91 | chunk.resize(0) 92 | new_sse_event.emit(headers, event_data.event, event_data.data) 93 | elif event_data.event != "continue_internal": 94 | chunk.resize(0) 95 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/functions/functions.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseFunctions 3 | extends Node 4 | 5 | 6 | signal task_error(error) 7 | 8 | const _AUTHORIZATION_HEADER := "Authorization: Bearer " 9 | 10 | const _MAX_POOLED_REQUEST_AGE = 30 11 | 12 | var request := -1 13 | 14 | var _base_url := "" 15 | 16 | var _http_request_pool : Array 17 | 18 | 19 | func _process(delta : float) -> void: 20 | for i in range(_http_request_pool.size() - 1, -1, -1): 21 | var request = _http_request_pool[i] 22 | if not request.get_meta("requesting"): 23 | var lifetime: float = request.get_meta("lifetime") + delta 24 | if lifetime > _MAX_POOLED_REQUEST_AGE: 25 | request.queue_free() 26 | _http_request_pool.remove_at(i) 27 | request.set_meta("lifetime", lifetime) 28 | 29 | 30 | func execute(function : String, method : int, params : Dictionary = {}, body : Dictionary = {}) -> FunctionTask: 31 | var function_task : FunctionTask = FunctionTask.new() 32 | function_task.task_error.connect(_on_task_error) 33 | 34 | function_task._method = method 35 | 36 | var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function 37 | function_task._url = url 38 | 39 | if not params.is_empty(): 40 | url += "?" 41 | for key in params.keys(): 42 | url += key + "=" + params[key] + "&" 43 | 44 | if not body.is_empty(): 45 | function_task._headers = PackedStringArray(["Content-Type: application/json"]) 46 | function_task._fields = JSON.stringify(body) 47 | 48 | _pooled_request(function_task) 49 | return function_task 50 | 51 | 52 | func _setup(config_json : Dictionary) -> void: 53 | _base_url = "https://" + config_json.functionsGeoZone + "-" + config_json.projectId + ".cloudfunctions.net/" 54 | 55 | 56 | func _pooled_request(task : FunctionTask) -> void: 57 | if Firebase.Auth.auth.is_empty(): 58 | Firebase._print("Unauthenticated request issued...") 59 | Firebase.Auth.login_anonymous() 60 | var result : Array = await Firebase.Auth.auth_request 61 | if result[0] != 1: 62 | _check_auth_error(result[0], result[1]) 63 | Firebase._print("Client connected as Anonymous") 64 | 65 | 66 | task._headers = Array(task._headers) + [_AUTHORIZATION_HEADER + Firebase.Auth.auth.idtoken] 67 | 68 | var http_request : HTTPRequest 69 | for request in _http_request_pool: 70 | if not request.get_meta("requesting"): 71 | http_request = request 72 | break 73 | 74 | if not http_request: 75 | http_request = HTTPRequest.new() 76 | _http_request_pool.append(http_request) 77 | add_child(http_request) 78 | http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) 79 | 80 | http_request.set_meta("requesting", true) 81 | http_request.set_meta("lifetime", 0.0) 82 | http_request.set_meta("task", task) 83 | http_request.request(task._url, task._headers, task._method, task._fields) 84 | 85 | 86 | # ------------- 87 | func _on_task_error(error): 88 | task_error.emit(error) 89 | Firebase._printerr(str(error)) 90 | 91 | 92 | func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: 93 | request.get_meta("task")._on_request_completed(result, response_code, headers, body) 94 | request.set_meta("requesting", false) 95 | 96 | 97 | func _check_auth_error(code : int, message : String) -> void: 98 | var err : String 99 | match code: 100 | 400: err = "Please, enable Anonymous Sign-in method or Authenticate the Client before issuing a request (best option)" 101 | Firebase._printerr(err) 102 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/firestore/firestore_task.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirestoreTask 3 | extends RefCounted 4 | 5 | 6 | #Connect to this signal to get them only from the individual FirestoreTask 7 | signal task_finished(task) 8 | signal document_added(doc) 9 | signal document_got(doc) 10 | signal document_updated(doc) 11 | signal document_deleted 12 | signal documents_listed(documents) 13 | signal result_query(result) 14 | signal task_error(error) 15 | 16 | enum Task { 17 | TASK_GET, 18 | TASK_POST, 19 | TASK_PATCH, 20 | TASK_DELETE, 21 | TASK_QUERY, 22 | TASK_LIST 23 | } 24 | 25 | var action := -1 : set = set_action 26 | 27 | var data 28 | var error 29 | var document : FirestoreDocument 30 | 31 | var _response_headers : PackedStringArray = PackedStringArray() 32 | var _response_code := 0 33 | 34 | var _method := -1 35 | var _url : String 36 | var _fields : String 37 | var _headers : PackedStringArray 38 | 39 | 40 | 41 | func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 42 | var bod 43 | if JSON.parse_string(body.get_string_from_utf8()) != null: 44 | bod = JSON.parse_string(body.get_string_from_utf8()) 45 | 46 | var failed : bool = bod == null and response_code != HTTPClient.RESPONSE_OK 47 | 48 | if response_code == HTTPClient.RESPONSE_OK: 49 | data = bod 50 | match action: 51 | Task.TASK_POST: 52 | document = FirestoreDocument.new(bod) 53 | document_added.emit(document) 54 | Task.TASK_GET: 55 | document = FirestoreDocument.new(bod) 56 | document_got.emit(document) 57 | Task.TASK_PATCH: 58 | document = FirestoreDocument.new(bod) 59 | document_updated.emit(document) 60 | Task.TASK_DELETE: 61 | document_deleted.emit() 62 | Task.TASK_QUERY: 63 | data = [] 64 | for doc in bod: 65 | if doc.has('document'): 66 | data.append(FirestoreDocument.new(doc.document)) 67 | result_query.emit(data) 68 | Task.TASK_LIST: 69 | data = [] 70 | if bod.has('documents'): 71 | for doc in bod.documents: 72 | data.append(FirestoreDocument.new(doc)) 73 | if bod.has("nextPageToken"): 74 | data.append(bod.nextPageToken) 75 | documents_listed.emit(data) 76 | else: 77 | error = bod 78 | task_error.emit(bod) 79 | 80 | task_finished.emit(self) 81 | 82 | 83 | func set_action(value : int) -> void: 84 | action = value 85 | match action: 86 | Task.TASK_GET, Task.TASK_LIST: 87 | _method = HTTPClient.METHOD_GET 88 | Task.TASK_POST, Task.TASK_QUERY: 89 | _method = HTTPClient.METHOD_POST 90 | Task.TASK_PATCH: 91 | _method = HTTPClient.METHOD_PATCH 92 | Task.TASK_DELETE: 93 | _method = HTTPClient.METHOD_DELETE 94 | 95 | 96 | func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: 97 | var ret := dic_a.duplicate(true) 98 | for key in dic_b: 99 | var val = dic_b[key] 100 | 101 | if val == null and nullify: 102 | ret.erase(key) 103 | elif val is Array: 104 | ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val) 105 | elif val is Dictionary: 106 | ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val) 107 | else: 108 | ret[key] = val 109 | return ret 110 | 111 | 112 | func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: 113 | var ret := arr_a.duplicate(true) 114 | ret.resize(len(arr_b)) 115 | 116 | var deletions := 0 117 | for i in len(arr_b): 118 | var index : int = i - deletions 119 | var val = arr_b[index] 120 | if val == null and nullify: 121 | ret.remove_at(index) 122 | deletions += i 123 | elif val is Array: 124 | ret[index] = _merge_array(ret[index] if ret[index] else [], val) 125 | elif val is Dictionary: 126 | ret[index] = _merge_dict(ret[index] if ret[index] else {}, val) 127 | else: 128 | ret[index] = val 129 | return ret 130 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/storage/storage_reference.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name StorageReference 3 | extends RefCounted 4 | 5 | 6 | const DEFAULT_MIME_TYPE := "application/octet-stream" 7 | 8 | const MIME_TYPES := { 9 | "bmp": "image/bmp", 10 | "css": "text/css", 11 | "csv": "text/csv", 12 | "gd": "text/plain", 13 | "htm": "text/html", 14 | "html": "text/html", 15 | "jpeg": "image/jpeg", 16 | "jpg": "image/jpeg", 17 | "json": "application/json", 18 | "mp3": "audio/mpeg", 19 | "mpeg": "video/mpeg", 20 | "ogg": "audio/ogg", 21 | "ogv": "video/ogg", 22 | "png": "image/png", 23 | "shader": "text/plain", 24 | "svg": "image/svg+xml", 25 | "tif": "image/tiff", 26 | "tiff": "image/tiff", 27 | "tres": "text/plain", 28 | "tscn": "text/plain", 29 | "txt": "text/plain", 30 | "wav": "audio/wav", 31 | "webm": "video/webm", 32 | "webp": "video/webm", 33 | "xml": "text/xml", 34 | } 35 | 36 | var bucket : String 37 | 38 | var full_path : String 39 | var name : String 40 | 41 | 42 | var parent : StorageReference 43 | 44 | var root : StorageReference 45 | 46 | var storage # FirebaseStorage (Can't static type due to cyclic reference) 47 | 48 | var valid := false 49 | 50 | 51 | func child(path : String) -> StorageReference: 52 | if not valid: 53 | return null 54 | return storage.ref(full_path + "/" + path) 55 | 56 | 57 | func put_data(data : PackedByteArray, metadata := {}) -> FirebaseStorageTask: 58 | if not valid: 59 | return null 60 | if not "Content-Length" in metadata and OS.get_name() != "HTML5": 61 | metadata["Content-Length"] = data.size() 62 | 63 | var headers := [] 64 | for key in metadata: 65 | headers.append(key + ": " + str(metadata[key])) 66 | 67 | return storage._upload(data, headers, self, false) 68 | 69 | 70 | func put_string(data : String, metadata := {}) -> FirebaseStorageTask: 71 | return put_data(data.to_utf8_buffer(), metadata) 72 | 73 | 74 | func put_file(file_path : String, metadata := {}) -> FirebaseStorageTask: 75 | var file = FileAccess.open(file_path, FileAccess.READ) 76 | var data := file.get_buffer(file.get_length()) 77 | 78 | if "Content-Type" in metadata: 79 | metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) 80 | 81 | return put_data(data, metadata) 82 | 83 | 84 | func get_data() -> FirebaseStorageTask: 85 | if not valid: 86 | return null 87 | storage._download(self, false, false) 88 | return storage._pending_tasks[-1] 89 | 90 | 91 | func get_string() -> FirebaseStorageTask: 92 | var task := get_data() 93 | task.task_finished.connect(_on_task_finished.bind(task, "stringify")) 94 | return task 95 | 96 | 97 | func get_download_url() -> FirebaseStorageTask: 98 | if not valid: 99 | return null 100 | return storage._download(self, false, true) 101 | 102 | 103 | func get_metadata() -> FirebaseStorageTask: 104 | if not valid: 105 | return null 106 | return storage._download(self, true, false) 107 | 108 | 109 | func update_metadata(metadata : Dictionary) -> FirebaseStorageTask: 110 | if not valid: 111 | return null 112 | var data := JSON.stringify(metadata).to_utf8_buffer() 113 | var headers := PackedStringArray(["Accept: application/json"]) 114 | return storage._upload(data, headers, self, true) 115 | 116 | 117 | func list() -> FirebaseStorageTask: 118 | if not valid: 119 | return null 120 | return storage._list(self, false) 121 | 122 | 123 | func list_all() -> FirebaseStorageTask: 124 | if not valid: 125 | return null 126 | return storage._list(self, true) 127 | 128 | 129 | func delete() -> FirebaseStorageTask: 130 | if not valid: 131 | return null 132 | return storage._delete(self) 133 | 134 | 135 | func _on_task_finished(task : FirebaseStorageTask, action : String) -> void: 136 | match action: 137 | "stringify": 138 | if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: 139 | task.data = task.data.get_string_from_utf8() 140 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/firestore/firestore_collection.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirestoreCollection 3 | extends RefCounted 4 | 5 | 6 | #Connect to this signal to get them independently of the individual FirestoreTask 7 | signal document_added(doc) 8 | signal document_got(doc) 9 | signal document_updated(doc) 10 | signal document_deleted 11 | signal error(code, status, message) 12 | 13 | const _AUTHORIZATION_HEADER := "Authorization: Bearer " 14 | 15 | const _separator := "/" 16 | const _query_tag := "?" 17 | const _documentId_tag := "documentId=" 18 | 19 | var collection_name : String 20 | var firestore : FirebaseFirestore 21 | 22 | var _base_url : String 23 | var _extended_url : String 24 | 25 | var _documents := {} 26 | var _request_queues := {} 27 | 28 | # ----------------------- Requests 29 | 30 | func get_doc(document_id : String) -> FirestoreTask: 31 | var task : FirestoreTask = FirestoreTask.new() 32 | task.action = FirestoreTask.Task.TASK_GET 33 | task.data = collection_name + "/" + document_id 34 | var url = _get_request_url() + _separator + document_id.replace(" ", "%20") 35 | 36 | task.document_got.connect(_on_document_got) 37 | task.task_finished.connect(_on_task_finished.bind(document_id),CONNECT_DEFERRED) 38 | _process_request(task, document_id, url) 39 | return task 40 | 41 | 42 | func add(document_id : String, fields : Dictionary = {}) -> FirestoreTask: 43 | var task : FirestoreTask = FirestoreTask.new() 44 | task.action = FirestoreTask.Task.TASK_POST 45 | task.data = collection_name + "/" + document_id 46 | var url = _get_request_url() + _query_tag + _documentId_tag + document_id 47 | 48 | task.document_added.connect(_on_document_added) 49 | task.task_finished.connect(_on_task_finished.bind(document_id),CONNECT_DEFERRED) 50 | _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) 51 | return task 52 | 53 | 54 | func update(document_id : String, fields : Dictionary = {}) -> FirestoreTask: 55 | var task : FirestoreTask = FirestoreTask.new() 56 | task.action = FirestoreTask.Task.TASK_PATCH 57 | task.data = collection_name + "/" + document_id 58 | var url = _get_request_url() + _separator + document_id.replace(" ", "%20") + "?" 59 | for key in fields.keys(): 60 | url += "updateMask.fieldPaths=" + key + "&" 61 | url = url.rstrip("&") 62 | 63 | task.document_updated.connect(_on_document_updated) 64 | task.task_finished.connect(_on_task_finished.bind(document_id),CONNECT_DEFERRED) 65 | _process_request(task, document_id, url, JSON.stringify(FirestoreDocument.dict2fields(fields))) 66 | return task 67 | 68 | 69 | func delete(document_id : String) -> FirestoreTask: 70 | var task : FirestoreTask = FirestoreTask.new() 71 | task.action = FirestoreTask.Task.TASK_DELETE 72 | task.data = collection_name + "/" + document_id 73 | var url = _get_request_url() + _separator + document_id.replace(" ", "%20") 74 | 75 | task.document_deleted.connect(_on_document_deleted) 76 | task.task_finished.connect(_on_task_finished.bind(document_id),CONNECT_DEFERRED) 77 | _process_request(task, document_id, url) 78 | return task 79 | 80 | 81 | # ----------------- Functions 82 | func _get_request_url() -> String: 83 | return _base_url + _extended_url + collection_name 84 | 85 | 86 | func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: 87 | if Firebase.Auth.auth.is_empty(): 88 | Firebase._print("Unauthenticated request issued...") 89 | Firebase.Auth.login_anonymous() 90 | var result : Array = await Firebase.Auth.auth_request 91 | if result[0] != 1: 92 | Firebase.Firestore._check_auth_error(result[0], result[1]) 93 | return 94 | Firebase._print("Client authenticated as Anonymous User.") 95 | 96 | task._url = url 97 | task._fields = fields 98 | task._headers = PackedStringArray([_AUTHORIZATION_HEADER + Firebase.Auth.auth.idtoken]) 99 | if _request_queues.has(document_id) and not _request_queues[document_id].is_empty(): 100 | _request_queues[document_id].append(task) 101 | else: 102 | _request_queues[document_id] = [] 103 | firestore._pooled_request(task) 104 | 105 | 106 | func _on_task_finished(task : FirestoreTask, document_id : String) -> void: 107 | if not _request_queues[document_id].is_empty(): 108 | task._push_request(task._url, _AUTHORIZATION_HEADER + Firebase.Auth.auth.idtoken, task._fields) 109 | 110 | 111 | func _on_document_got(document : FirestoreDocument): 112 | document_got.emit(document) 113 | 114 | 115 | func _on_document_added(document : FirestoreDocument): 116 | document_added.emit(document) 117 | 118 | 119 | func _on_document_updated(document : FirestoreDocument): 120 | document_updated.emit(document) 121 | 122 | 123 | func _on_document_deleted(): 124 | document_deleted.emit() 125 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/firestore/firestore_document.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirestoreDocument 3 | extends RefCounted 4 | 5 | 6 | var document : Dictionary # the Document itself 7 | var doc_fields : Dictionary # only .fields 8 | var doc_name : String # only .name 9 | var create_time : String # createTime 10 | 11 | 12 | func _init(doc : Dictionary = {},_doc_name : String = "",_doc_fields : Dictionary = {}): 13 | document = doc 14 | doc_name = doc.name 15 | if doc_name.count("/") > 2: 16 | doc_name = (doc_name.split("/") as Array).back() 17 | doc_fields = fields2dict(document) 18 | create_time = doc.createTime 19 | 20 | 21 | static func dict2fields(dict : Dictionary) -> Dictionary: 22 | var fields : Dictionary 23 | var var_type : String 24 | for field in dict.keys(): 25 | var field_value = dict[field] 26 | if "." in field: 27 | var keys: Array = field.split(".") 28 | field = keys.pop_front() 29 | keys.reverse() 30 | for key in keys: 31 | field_value = { key : field_value } 32 | match typeof(field_value): 33 | TYPE_NIL: var_type = "nullValue" 34 | TYPE_BOOL: var_type = "booleanValue" 35 | TYPE_INT: var_type = "integerValue" 36 | TYPE_FLOAT: var_type = "doubleValue" 37 | TYPE_STRING: var_type = "stringValue" 38 | TYPE_DICTIONARY: 39 | if is_field_timestamp(field_value): 40 | var_type = "timestampValue" 41 | field_value = dict2timestamp(field_value) 42 | else: 43 | var_type = "mapValue" 44 | field_value = dict2fields(field_value) 45 | TYPE_ARRAY: 46 | var_type = "arrayValue" 47 | field_value = {"values": array2fields(field_value)} 48 | 49 | if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"): 50 | for key in field_value.fields.keys(): 51 | fields[field].mapValue.fields[key] = field_value.fields[key] 52 | else: 53 | fields[field] = { var_type : field_value } 54 | return {'fields' : fields} 55 | 56 | 57 | static func fields2dict(doc : Dictionary) -> Dictionary: 58 | var dict : Dictionary 59 | if doc.has("fields"): 60 | for field in (doc.fields).keys(): 61 | if (doc.fields)[field].has("mapValue"): 62 | dict[field] = fields2dict((doc.fields)[field].mapValue) 63 | elif (doc.fields)[field].has("timestampValue"): 64 | dict[field] = timestamp2dict((doc.fields)[field].timestampValue) 65 | elif (doc.fields)[field].has("arrayValue"): 66 | dict[field] = fields2array((doc.fields)[field].arrayValue) 67 | elif (doc.fields)[field].has("integerValue"): 68 | dict[field] = (doc.fields)[field].values()[0] as int 69 | elif (doc.fields)[field].has("doubleValue"): 70 | dict[field] = (doc.fields)[field].values()[0] as float 71 | elif (doc.fields)[field].has("booleanValue"): 72 | dict[field] = (doc.fields)[field].values()[0] as bool 73 | elif (doc.fields)[field].has("nullValue"): 74 | dict[field] = null 75 | else: 76 | dict[field] = (doc.fields)[field].values()[0] 77 | return dict 78 | 79 | 80 | static func array2fields(array : Array) -> Array: 81 | var fields : Array 82 | var var_type : String 83 | for field in array: 84 | match typeof(field): 85 | TYPE_DICTIONARY: 86 | if is_field_timestamp(field): 87 | var_type = "timestampValue" 88 | field = dict2timestamp(field) 89 | else: 90 | var_type = "mapValue" 91 | field = dict2fields(field) 92 | TYPE_NIL: var_type = "nullValue" 93 | TYPE_BOOL: var_type = "booleanValue" 94 | TYPE_INT: var_type = "integerValue" 95 | TYPE_FLOAT: var_type = "doubleValue" 96 | TYPE_STRING: var_type = "stringValue" 97 | TYPE_ARRAY: var_type = "arrayValue" 98 | 99 | fields.append({ var_type : field }) 100 | return fields 101 | 102 | 103 | static func fields2array(array : Dictionary) -> Array: 104 | var fields : Array 105 | if array.has("values"): 106 | for field in array.values: 107 | var item 108 | match field.keys()[0]: 109 | "mapValue": 110 | item = fields2dict(field.mapValue) 111 | "arrayValue": 112 | item = fields2array(field.arrayValue) 113 | "integerValue": 114 | item = field.values()[0] as int 115 | "doubleValue": 116 | item = field.values()[0] as float 117 | "booleanValue": 118 | item = field.values()[0] as bool 119 | "timestampValue": 120 | item = timestamp2dict(field.timestampValue) 121 | "nullValue": 122 | item = null 123 | _: 124 | item = field.values()[0] 125 | fields.append(item) 126 | return fields 127 | 128 | 129 | static func dict2timestamp(dict : Dictionary) -> String: 130 | dict.erase('weekday') 131 | dict.erase('dst') 132 | var dict_values : Array = dict.values() 133 | return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values 134 | 135 | 136 | static func timestamp2dict(timestamp : String) -> Dictionary: 137 | var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} 138 | var dict : PackedStringArray = timestamp.split("T")[0].split("-") 139 | dict.append_array(timestamp.split("T")[1].split(":")) 140 | for value in dict.size() : 141 | datetime[datetime.keys()[value]] = int(dict[value]) 142 | return datetime 143 | 144 | 145 | static func is_field_timestamp(field : Dictionary) -> bool: 146 | return field.has_all(['year','month','day','hour','minute','second']) 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # Firebase API for Godot 4 4 | 5 | Adds Firebase connectivity to your Godot 4 project. 6 | 7 |
8 | 9 | ## Install 10 | 11 | Make sure there's no other `Firebase` plugin/autoload in your project. 12 | 13 | 1. Put the `addons` folder in your project's main directory 14 | 2. Enable the plugin in `Project > Project Settings > Plugins` 15 | 16 | If it's enabled but you still have no `Firebase` in your autoloads, disable and re-enable it again. 17 | 18 |
19 | 20 | --- 21 |
22 | 23 | 24 | ## Use 25 | 26 | Unlike [GodotFirebase](https://github.com/GodotNuts/GodotFirebase), on which this library is based, you initialize it manually. 27 | 28 | Add this to your `main scene` and enter your [Firebase configuration](https://support.google.com/firebase/answer/7015592?hl=en#zippy=%2Cin-this-article) as fields of the corresponding keys in `firebaseConfig`: 29 | 30 | ``` 31 | const firebaseConfig = { 32 | "apiKey": "", 33 | "authDomain": "", 34 | "databaseURL": "", 35 | "projectId": "", 36 | "storageBucket": "", 37 | "messagingSenderId": "", 38 | "appId": "", 39 | "measurementId": "" 40 | } 41 | 42 | func _ready(): 43 | Firebase.setup_modules(firebaseConfig) 44 | ``` 45 | 46 | This approach allows you to have multiple Firebase configurations in the same application and apply them at runtime. Like this: 47 | ``` 48 | const configs = { 49 | "config1" : {...}, 50 | "config2" : {...} 51 | } 52 | 53 | func _ready(): 54 | Firebase.setup_modules(configs.config1) 55 | do_some_things() 56 | Firebase.setup_modules(configs.config2) 57 | do_other_things() 58 | ``` 59 | 60 |
61 | 62 | --- 63 |
64 | 65 | ## Differences with GodotFirebase 66 | Despite a slightly different architecture, Firebase API 4.x inherited most of the original methods and signals, so you can refer to the original [wiki](https://github.com/GodotNuts/GodotFirebase/wiki) for guidance. 67 | 68 | The only differences in nomenclature are as follows: 69 | 70 |
71 | 72 | `FirestoreCollection` class: 73 | 74 | |GodotFirebase|Firebase API 4.x| 75 | |-|-| 76 | |`func get(...)` *|`func get_doc(...)`| 77 | 78 | * get() is reserved in Godot 4 79 | 80 |
81 | 82 | `FirestoreCollection` and `FirestoreTask` classes: 83 | 84 | |GodotFirebase|Firebase API 4.x| 85 | |-|-| 86 | |`signal add_document(doc)`|`signal document_added(doc)`| 87 | |`signal get_document(doc)`|`signal document_got(doc)`| 88 | |`signal update_document(doc)`|`signal document_updated(doc)`| 89 | |`signal delete_document`|`signal document_deleted`| 90 | 91 |
92 | 93 | `Firestore` and `FirestoreTask` classes: 94 | |GodotFirebase|Firebase API 4.x| 95 | |-|-| 96 | |`signal listed_documents(docs)`|`signal documents_listed(docs)`| 97 | 98 |
99 | 100 | ## Firebase Realtime Database 101 | 102 | Besides initialization, the only significant usage-wise difference with GodotFirebase involves Firebase Realtime Database. 103 | 104 | To initialize a Realtime Database reference, use this method of `FirebaseRealtime`: 105 | ``` 106 | func get_realtime_reference(path := "", filter := {}) -> FirebaseRealtimeReference 107 | ``` 108 | 109 | Providing a path is optional: if you don't, the reference will work through the entire database. 110 | 111 |
112 | 113 | The following will allow you to listen for changes in the database (or the given path): 114 | ``` 115 | func _ready(): 116 | var path : String 117 | var ref = Firebase.Realtime.get_realtime_reference(path) 118 | ref.new_data_update.connect(myfunc) 119 | 120 | func myfunc(update): 121 | print(update.data) 122 | ``` 123 | 124 | The signal `new_data_update` is emitted everytime there's a change in the referenced path in your database. 125 | 126 | Please note that when a new key is *created* in the database, the creation event itself will **not** fire an *update* signal. 127 | 128 |
129 | 130 | A path is optional when calling `FirebaseRealtimeReference.update()` too. The method will just use the reference's path, if possible. 131 | 132 | 133 | 134 | Examples: 135 | 136 | ``` 137 | func _ready(): 138 | var ref = Firebase.Realtime.get_realtime_reference() 139 | var somedata = {...} 140 | ref.update(somedata, "path") 141 | ``` 142 | This will update `root/path`. 143 | 144 |
145 | 146 | ``` 147 | func _ready(): 148 | var ref = Firebase.Realtime.get_realtime_reference("path") 149 | var somedata = {...} 150 | ref.update(somedata) 151 | ``` 152 | This will update `root/path` too. 153 | 154 |
155 | 156 | ``` 157 | func _ready(): 158 | var ref = Firebase.Realtime.get_realtime_reference("path") 159 | var somedata = {...} 160 | ref.update(somedata, "more_path") 161 | ``` 162 | This will update `root/path/more_path`. 163 | 164 |
165 | 166 | Trying to `update()` the database root with a non-Dictionary value will result in a soft error: 167 | ``` 168 | func _ready(): 169 | # don't do this 170 | var ref = Firebase.Realtime.get_realtime_reference() # no initial path is provided 171 | ref.update("asdasd") # no additional path is provided and data is not a Dictionary 172 | 173 | # will print an error 174 | ``` 175 |
176 | 177 | Be aware that calling `delete()` in a reference with no initial path will result in the complete deletion of the database's content. 178 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/firestore/firestore.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseFirestore 3 | extends Node 4 | 5 | 6 | const _API_VERSION := "v1" 7 | 8 | signal documents_listed(documents) 9 | signal result_query(result) 10 | signal task_error(error) 11 | 12 | enum Requests { 13 | NONE = -1, 14 | LIST, 15 | QUERY 16 | } 17 | 18 | const _AUTHORIZATION_HEADER := "Authorization: Bearer " 19 | 20 | const _MAX_POOLED_REQUEST_AGE = 30 21 | 22 | var request := -1 23 | 24 | var persistence_enabled := true 25 | 26 | var collections : Dictionary 27 | 28 | ## A Dictionary containing all authentication fields for the current logged user. 29 | 30 | var _base_url : String 31 | var _extended_url : String 32 | var _query_suffix := ":runQuery" 33 | 34 | var _request_list_node : HTTPRequest 35 | var _requests_queue : Array 36 | var _current_query : FirestoreQuery 37 | 38 | var _http_request_pool := [] 39 | 40 | 41 | func _process(delta : float) -> void: 42 | for i in range(_http_request_pool.size() - 1, -1, -1): 43 | var request = _http_request_pool[i] 44 | if not request.get_meta("requesting"): 45 | var lifetime: float = request.get_meta("lifetime") + delta 46 | if lifetime > _MAX_POOLED_REQUEST_AGE: 47 | request.queue_free() 48 | _http_request_pool.remove_at(i) 49 | request.set_meta("lifetime", lifetime) 50 | 51 | 52 | func collection(path : String) -> FirestoreCollection: 53 | if not collections.has(path): 54 | var coll : FirestoreCollection = FirestoreCollection.new() 55 | coll._extended_url = _extended_url 56 | coll._base_url = _base_url 57 | coll.collection_name = path 58 | coll.firestore = self 59 | collections[path] = coll 60 | return coll 61 | else: 62 | return collections[path] 63 | 64 | 65 | func query(query : FirestoreQuery) -> FirestoreTask: 66 | var firestore_task : FirestoreTask = FirestoreTask.new() 67 | firestore_task.result_query.connect(_on_result_query) 68 | firestore_task.task_error.connect(_on_task_error) 69 | firestore_task.action = FirestoreTask.Task.TASK_QUERY 70 | var body : Dictionary = { structuredQuery = query.query } 71 | var url : String = _base_url + _extended_url + _query_suffix 72 | 73 | firestore_task.data = query 74 | firestore_task._fields = JSON.stringify(body) 75 | firestore_task._url = url 76 | _pooled_request(firestore_task) 77 | return firestore_task 78 | 79 | 80 | func list(path : String, page_size : int = 0, page_token := "", order_by := "") -> FirestoreTask: 81 | var firestore_task := FirestoreTask.new() 82 | firestore_task.documents_listed.connect(_on_documents_listed) 83 | firestore_task.task_error.connect(_on_task_error) 84 | firestore_task.action = FirestoreTask.Task.TASK_LIST 85 | var url : String = _base_url + _extended_url + path 86 | if page_size != 0: 87 | url += "?pageSize=" + str(page_size) 88 | if page_token != "": 89 | url += "&pageToken=" + page_token 90 | if order_by != "": 91 | url += "&orderBy=" + order_by 92 | 93 | firestore_task.data = [path, page_size, page_token, order_by] 94 | firestore_task._url = url 95 | _pooled_request(firestore_task) 96 | return firestore_task 97 | 98 | 99 | func _setup(config_json : Dictionary) -> void: 100 | _extended_url = "projects/" + Firebase._config.projectId + "/databases/(default)/documents/" 101 | _base_url = "https://firestore.googleapis.com/" + _API_VERSION + "/" 102 | 103 | 104 | func _pooled_request(task : FirestoreTask) -> void: 105 | if Firebase.Auth.auth.is_empty(): 106 | Firebase._print("Unauthenticated request issued...") 107 | Firebase.Auth.login_anonymous() 108 | var result : Array = await Firebase.Auth.auth_request 109 | if result[0] != 1: 110 | _check_auth_error(result[0], result[1]) 111 | Firebase._print("Client connected as Anonymous") 112 | 113 | task._headers = PackedStringArray([_AUTHORIZATION_HEADER + Firebase.Auth.auth.idtoken]) 114 | 115 | var http_request : HTTPRequest 116 | for request in _http_request_pool: 117 | if not request.get_meta("requesting"): 118 | http_request = request 119 | break 120 | 121 | if not http_request: 122 | http_request = HTTPRequest.new() 123 | http_request.timeout = 5 124 | _http_request_pool.append(http_request) 125 | add_child(http_request) 126 | http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) 127 | 128 | http_request.set_meta("requesting", true) 129 | http_request.set_meta("lifetime", 0.0) 130 | http_request.set_meta("task", task) 131 | http_request.request(task._url, task._headers, task._method, task._fields) 132 | 133 | 134 | # ------------- 135 | 136 | 137 | func _on_documents_listed(_listed_documents : Array): 138 | documents_listed.emit(_listed_documents) 139 | 140 | 141 | func _on_result_query(result : Array): 142 | result_query.emit(result) 143 | 144 | 145 | func _on_task_error(error): 146 | task_error.emit(error) 147 | Firebase._printerr(str(error)) 148 | 149 | 150 | func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: 151 | request.get_meta("task")._on_request_completed(result, response_code, headers, body) 152 | request.set_meta("requesting", false) 153 | 154 | 155 | func _check_auth_error(code : int, message : String) -> void: 156 | var err : String 157 | match code: 158 | 400: err = "Please, enable Anonymous Sign-in method or Authenticate the Client before issuing a request (best option)" 159 | Firebase._printerr(err) 160 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/realtime/reference.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name RealtimeReference 3 | extends Node 4 | 5 | 6 | signal new_data_update(_update : Dictionary) 7 | signal patch_data_update(_update : Dictionary) 8 | signal delete_data_update(_update : Dictionary) 9 | 10 | signal push_successful() 11 | signal push_failed() 12 | 13 | signal snapshot(snapshot_data : Dictionary) 14 | 15 | var _pusher : HTTPRequest 16 | var _listener : HTTPSSEClient 17 | var _getter : HTTPRequest 18 | var _filter_query : Dictionary 19 | var _db_path : String 20 | var _cached_filter : String 21 | var _push_queue : Array 22 | var _update_queue : Array 23 | var _delete_queue : Array 24 | var _can_connect_to_host := false 25 | 26 | var _headers : PackedStringArray 27 | 28 | 29 | func setup(path : String, filter_query_dict : Dictionary, pusher_ref : HTTPRequest, listener_ref : HTTPSSEClient, getter_ref : HTTPRequest) -> void: 30 | _db_path = path 31 | _filter_query = filter_query_dict 32 | 33 | _pusher = pusher_ref 34 | _pusher.request_completed.connect(on_push_request_complete) 35 | add_child(_pusher) 36 | 37 | _listener = listener_ref 38 | _listener.new_sse_event.connect(on_new_sse_event) 39 | var base_url = _get_list_url(false).trim_suffix("/") 40 | var extended_url = "/" + _db_path + _get_remaining_path(false) 41 | var port = -1 42 | _listener.set_coordinates(base_url, extended_url, port) 43 | add_child(_listener) 44 | 45 | _getter = getter_ref 46 | add_child(_getter) 47 | _getter.request_completed.connect(on_snapshot_complete) 48 | 49 | 50 | 51 | func _get_list_url(with_port := true) -> String: 52 | var url = Firebase._config.databaseURL.trim_suffix("/") 53 | return url + "/" 54 | 55 | 56 | func _get_remaining_path(is_push := true) -> String: 57 | var remaining_path : String 58 | if _filter_query.is_empty() or is_push: 59 | remaining_path = ".json?auth=" + Firebase.Auth.auth.idtoken 60 | else: 61 | remaining_path = ".json?" + _get_filter() + "&auth=" + Firebase.Auth.auth.idtoken 62 | 63 | return remaining_path 64 | 65 | 66 | func _get_filter(): 67 | if _filter_query.is_empty(): 68 | return "" 69 | if _cached_filter.is_empty(): 70 | _cached_filter = "" 71 | if _filter_query.has("orderBy"): 72 | _cached_filter += "orderBy=" + '"' + _filter_query.orderBy + '"' 73 | _filter_query.erase("orderBy") 74 | else: 75 | _cached_filter += "orderBy=" + '"$key"' 76 | for key in _filter_query.keys(): 77 | _cached_filter += "&" + key + "=" + str(_filter_query[key]) 78 | 79 | return _cached_filter 80 | 81 | 82 | ######## LISTEN 83 | func on_new_sse_event(headers : Dictionary, event : String, data) -> void: 84 | if event != "keep-alive": 85 | var _update := {"data" : data, "path" : _db_path} 86 | if event == "put": 87 | new_data_update.emit(_update) 88 | elif event == "patch": 89 | patch_data_update.emit(_update) 90 | elif event == "delete": 91 | delete_data_update.emit(_update) 92 | 93 | 94 | func get_snapshot(path := "") -> Dictionary: 95 | var remaining := _get_remaining_path() 96 | var ref_pos = _get_list_url() + _db_path + "/" + path + remaining 97 | _getter.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") 98 | return await snapshot 99 | 100 | 101 | 102 | ######## PUSH 103 | # Puts data in your reference's root with an automatically generated ID 104 | func push(data) -> void: 105 | var to_push = JSON.stringify(data) 106 | if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: 107 | _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) 108 | else: 109 | _push_queue.append(data) 110 | 111 | 112 | # Puts/updates data in the given path 113 | func update(data : Dictionary, path : String = "") -> void: 114 | path = path.strip_edges(true, true) 115 | 116 | if path == "/": 117 | path = "" 118 | 119 | var to_update = JSON.stringify(data) 120 | var status = _pusher.get_http_client_status() 121 | if status == HTTPClient.STATUS_DISCONNECTED || status != HTTPClient.STATUS_REQUESTING: 122 | var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) 123 | 124 | _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) 125 | else: 126 | _update_queue.append({"path": path, "data": data}) 127 | 128 | 129 | # Deletes data in the given path 130 | func delete(path : String = "") -> void: 131 | if _pusher.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: 132 | _pusher.request(_get_list_url() + _db_path + "/" + path + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") 133 | else: 134 | _delete_queue.append(path) 135 | 136 | 137 | func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 138 | if response_code == HTTPClient.RESPONSE_OK: 139 | push_successful.emit() 140 | else: 141 | push_failed.emit() 142 | 143 | if _push_queue.size() > 0: 144 | push(_push_queue.pop_front()) 145 | return 146 | if _update_queue.size() > 0: 147 | var e = _update_queue.pop_front() 148 | update(e.data, e.path) 149 | return 150 | if _delete_queue.size() > 0: 151 | delete(_delete_queue.pop_front()) 152 | 153 | 154 | func on_snapshot_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 155 | if response_code == HTTPClient.RESPONSE_OK: 156 | var bod = body 157 | if body is PackedByteArray: 158 | bod = bod.get_string_from_utf8() 159 | snapshot.emit(JSON.parse_string(bod)) 160 | else: 161 | snapshot.emit({"ERROR" : "Snapshot failed!"}) 162 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/firestore/firestore_query.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | class_name FirestoreQuery 4 | 5 | 6 | class Order: 7 | var obj : Dictionary 8 | 9 | class Cursor: 10 | var values : Array 11 | var before := false 12 | 13 | func _init(v : Array, b : bool): 14 | values = v 15 | before = b 16 | 17 | signal query_result(query_result) 18 | 19 | const TEMPLATE_QUERY := { 20 | select = {}, 21 | from = [], 22 | where = {}, 23 | orderBy = [], 24 | startAt = {}, 25 | endAt = {}, 26 | offset = 0, 27 | limit = 0 28 | } 29 | 30 | var query : Dictionary 31 | 32 | enum OPERATOR { 33 | # Standard operators 34 | OPERATOR_NSPECIFIED, 35 | LESS_THAN, 36 | LESS_THAN_OR_EQUAL, 37 | GREATER_THAN, 38 | GREATER_THAN_OR_EQUAL, 39 | EQUAL, 40 | NOT_EQUAL, 41 | ARRAY_CONTAINS, 42 | ARRAY_CONTAINS_ANY, 43 | IN, 44 | NOT_IN, 45 | 46 | # Unary operators 47 | IS_NAN, 48 | IS_NULL, 49 | IS_NOT_NAN, 50 | IS_NOT_NULL, 51 | 52 | # Complex operators 53 | AND, 54 | OR 55 | } 56 | 57 | enum DIRECTION { 58 | DIRECTION_UNSPECIFIED, 59 | ASCENDING, 60 | DESCENDING 61 | } 62 | 63 | 64 | # Select which fields you want to return as a reflection from your query. 65 | # Fields must be added inside a list. Only a field is accepted inside the list 66 | # Leave the Array empty if you want to return the whole document 67 | func select(fields) -> FirestoreQuery: 68 | match typeof(fields): 69 | TYPE_STRING: 70 | query.select = { fields = { fieldPath = fields } } 71 | TYPE_ARRAY: 72 | for field in fields: 73 | field = ({ fieldPath = field }) 74 | query.select = { fields = fields } 75 | _: 76 | print("Type of 'fields' is not accepted.") 77 | return self 78 | 79 | 80 | # Select the collection you want to return the query result from 81 | # if @all_descendants also sub-collections will be returned. If false, only documents will be returned 82 | func from(collection_id : String, all_descendants := true) -> FirestoreQuery: 83 | query.from = [{collectionId = collection_id, allDescendants = all_descendants}] 84 | return self 85 | 86 | 87 | func from_many(collections_array : Array) -> FirestoreQuery: 88 | var collections : Array 89 | for collection in collections_array: 90 | collections.append({collectionId = collection[0], allDescendants = collection[1]}) 91 | query.from = collections.duplicate(true) 92 | return self 93 | 94 | 95 | func where(field : String, operator : int, value = null, chain : int = -1): 96 | if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: 97 | if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): 98 | var filters : Array 99 | if query.has("where") and query.where.has("compositeFilter"): 100 | if chain == -1: 101 | filters = query.where.compositeFilter.filters.duplicate(true) 102 | chain = OPERATOR.get(query.where.compositeFilter.op) 103 | else: 104 | filters.append(query.where) 105 | filters.append(create_unary_filter(field, operator)) 106 | query.where = create_composite_filter(chain, filters) 107 | else: 108 | query.where = create_unary_filter(field, operator) 109 | else: 110 | if value == null: 111 | print("A value must be defined to match the field: " + field) 112 | else: 113 | if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): 114 | var filters : Array 115 | if query.has("where") and query.where.has("compositeFilter"): 116 | if chain == -1: 117 | filters = query.where.compositeFilter.filters.duplicate(true) 118 | chain = OPERATOR.get(query.where.compositeFilter.op) 119 | else: 120 | filters.append(query.where) 121 | filters.append(create_field_filter(field, operator, value)) 122 | query.where = create_composite_filter(chain, filters) 123 | else: 124 | query.where = create_field_filter(field, operator, value) 125 | return self 126 | 127 | 128 | # default direction = Ascending 129 | func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> FirestoreQuery: 130 | query.orderBy = [_order_object(field, direction).obj] 131 | return self 132 | 133 | 134 | # Order by a set of fields and directions 135 | # @order_list is an Array of Arrays with the following structure 136 | # [@field_name , @DIRECTION.[direction]] 137 | # else, order_object() can be called to return an already parsed Dictionary 138 | func order_by_fields(order_field_list : Array) -> FirestoreQuery: 139 | var order_list : Array 140 | for order in order_field_list: 141 | if order is Array: 142 | order_list.append(_order_object(order[0], order[1]).obj) 143 | elif order is Order: 144 | order_list.append(order.obj) 145 | query.orderBy = order_list 146 | return self 147 | 148 | 149 | func start_at(value, before : bool) -> FirestoreQuery: 150 | var cursor : Cursor = _cursor_object(value, before) 151 | query.startAt = { values = cursor.values, before = cursor.before } 152 | print(query.startAt) 153 | return self 154 | 155 | 156 | func end_at(value, before : bool) -> FirestoreQuery: 157 | var cursor : Cursor = _cursor_object(value, before) 158 | query.startAt = { values = cursor.values, before = cursor.before } 159 | print(query.startAt) 160 | return self 161 | 162 | 163 | func offset(offset : int) -> FirestoreQuery: 164 | if offset < 0: 165 | print("If specified, offset must be >= 0") 166 | else: 167 | query.offset = offset 168 | return self 169 | 170 | 171 | func limit(limit : int) -> FirestoreQuery: 172 | if limit < 0: 173 | print("If specified, offset must be >= 0") 174 | else: 175 | query.limit = limit 176 | return self 177 | 178 | 179 | 180 | # UTILITIES ---------------------------------------- 181 | 182 | static func _cursor_object(value, before : bool) -> Cursor: 183 | var parse : Dictionary = FirestoreDocument.dict2fields({value = value}).fields.value 184 | var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before) 185 | return cursor 186 | 187 | 188 | static func _order_object(field : String, direction : int) -> Order: 189 | var order : Order = Order.new() 190 | order.obj = { field = { fieldPath = field }, direction = DIRECTION.keys()[direction] } 191 | return order 192 | 193 | 194 | func create_field_filter(field : String, operator : int, value) -> Dictionary: 195 | return { 196 | fieldFilter = { 197 | field = { fieldPath = field }, 198 | op = OPERATOR.keys()[operator], 199 | value = FirestoreDocument.dict2fields({value = value}).fields.value 200 | } } 201 | 202 | 203 | func create_unary_filter(field : String, operator : int) -> Dictionary: 204 | return { 205 | unaryFilter = { 206 | field = { fieldPath = field }, 207 | op = OPERATOR.keys()[operator], 208 | } } 209 | 210 | 211 | func create_composite_filter(operator : int, filters : Array) -> Dictionary: 212 | return { 213 | compositeFilter = { 214 | op = OPERATOR.keys()[operator], 215 | filters = filters 216 | } } 217 | 218 | 219 | func clean() -> void: 220 | query = { } 221 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/storage/storage.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseStorage 3 | extends Node 4 | 5 | 6 | const _API_VERSION := "v0" 7 | 8 | signal task_successful(result, response_code, data) 9 | signal task_failed(result, response_code, data) 10 | 11 | ## The current storage bucket the Storage API is referencing. 12 | var bucket : String 13 | 14 | ## Whether a task is currently being processed. 15 | var requesting := false 16 | 17 | var _references : Dictionary 18 | 19 | var _base_url : String 20 | var _extended_url := "/[API_VERSION]/b/[APP_ID]/o/[FILE_PATH]" 21 | var _root_ref : StorageReference 22 | 23 | var _http_client : HTTPClient = HTTPClient.new() 24 | var _pending_tasks : Array 25 | 26 | var _current_task : FirebaseStorageTask 27 | var _response_code : int 28 | var _response_headers : PackedStringArray 29 | var _response_data : PackedByteArray 30 | var _content_length : int 31 | var _reading_body : bool 32 | 33 | 34 | func _notification(what : int) -> void: 35 | if what == NOTIFICATION_INTERNAL_PROCESS: 36 | _internal_process(get_process_delta_time()) 37 | 38 | 39 | func _internal_process(_delta : float) -> void: 40 | if not requesting: 41 | set_process_internal(false) 42 | return 43 | 44 | var task = _current_task 45 | 46 | match _http_client.get_status(): 47 | HTTPClient.STATUS_DISCONNECTED: 48 | _http_client.connect_to_host(_base_url, 443) 49 | 50 | HTTPClient.STATUS_RESOLVING, \ 51 | HTTPClient.STATUS_REQUESTING, \ 52 | HTTPClient.STATUS_CONNECTING: 53 | _http_client.poll() 54 | 55 | HTTPClient.STATUS_CONNECTED: 56 | var err := _http_client.request_raw(task._method, task._url, task._headers, task.data) 57 | if err: 58 | _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) 59 | 60 | HTTPClient.STATUS_BODY: 61 | if _http_client.has_response() or _reading_body: 62 | _reading_body = true 63 | 64 | # If there is a response... 65 | if _response_headers.is_empty(): 66 | _response_headers = _http_client.get_response_headers() # Get response headers. 67 | _response_code = _http_client.get_response_code() 68 | 69 | for header in _response_headers: 70 | if "Content-Length" in header: 71 | _content_length = header.trim_prefix("Content-Length: ").to_int() 72 | 73 | _http_client.poll() 74 | var chunk = _http_client.read_response_body_chunk() # Get a chunk. 75 | if chunk.size() == 0: 76 | # Got nothing, wait for buffers to fill a bit. 77 | pass 78 | else: 79 | _response_data += chunk # Append to read buffer. 80 | if _content_length != 0: 81 | task.progress = float(_response_data.size()) / _content_length 82 | 83 | if _http_client.get_status() != HTTPClient.STATUS_BODY: 84 | task.progress = 1.0 85 | _finish_request(HTTPRequest.RESULT_SUCCESS) 86 | else: 87 | task.progress = 1.0 88 | _finish_request(HTTPRequest.RESULT_SUCCESS) 89 | 90 | HTTPClient.STATUS_CANT_CONNECT: 91 | _finish_request(HTTPRequest.RESULT_CANT_CONNECT) 92 | HTTPClient.STATUS_CANT_RESOLVE: 93 | _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) 94 | HTTPClient.STATUS_CONNECTION_ERROR: 95 | _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) 96 | HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: 97 | _finish_request(HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR) 98 | 99 | 100 | ## 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. 101 | func ref(path := "") -> StorageReference: 102 | if _base_url == "": 103 | return null 104 | 105 | # Create a root storage reference if there's none 106 | # and we're not making one. 107 | if path != "" and not _root_ref: 108 | _root_ref = ref() 109 | 110 | path = _simplify_path(path) 111 | if not _references.has(path): 112 | var ref := StorageReference.new() 113 | _references[path] = ref 114 | ref.valid = true 115 | ref.bucket = bucket 116 | ref.full_path = path 117 | ref.name = path.get_file() 118 | ref.parent = ref(path + "/" + "..") 119 | ref.root = _root_ref 120 | ref.storage = self 121 | return ref 122 | else: 123 | return _references[path] 124 | 125 | 126 | func _setup(config_json : Dictionary) -> void: 127 | _base_url = "https://firebasestorage.googleapis.com" 128 | if bucket != config_json.storageBucket: 129 | bucket = config_json.storageBucket 130 | _http_client.close() 131 | 132 | 133 | func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> FirebaseStorageTask: 134 | if _base_url == "" or Firebase.Auth.auth.is_empty(): 135 | return null 136 | 137 | var task := FirebaseStorageTask.new() 138 | task.ref = ref 139 | task._url = _get_file_url(ref) 140 | task.action = FirebaseStorageTask.Task.TASK_UPLOAD_META if meta_only else FirebaseStorageTask.Task.TASK_UPLOAD 141 | task._headers = headers 142 | task.data = data 143 | _process_request(task) 144 | return task 145 | 146 | 147 | func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> FirebaseStorageTask: 148 | if _base_url == "" or Firebase.Auth.auth.is_empty(): 149 | return null 150 | 151 | var info_task := FirebaseStorageTask.new() 152 | info_task.ref = ref 153 | info_task._url = _get_file_url(ref) 154 | info_task.action = FirebaseStorageTask.Task.TASK_DOWNLOAD_URL if url_only else FirebaseStorageTask.Task.TASK_DOWNLOAD_META 155 | _process_request(info_task) 156 | 157 | if url_only or meta_only: 158 | return info_task 159 | 160 | var task := FirebaseStorageTask.new() 161 | task.ref = ref 162 | task._url = _get_file_url(ref) + "?alt=media&token=" 163 | task.action = FirebaseStorageTask.Task.TASK_DOWNLOAD 164 | _pending_tasks.append(task) 165 | 166 | await info_task.task_finished 167 | if info_task.data and not "error" in info_task.data: 168 | task._url += info_task.data.downloadTokens 169 | else: 170 | task.data = info_task.data 171 | task.response_headers = info_task.response_headers 172 | task.response_code = info_task.response_code 173 | task.result = info_task.result 174 | task.finished = true 175 | task.task_finished.emit() 176 | task_failed.emit(task.result, task.response_code, task.data) 177 | _pending_tasks.erase(task) 178 | 179 | return task 180 | 181 | 182 | func _list(ref : StorageReference, list_all : bool) -> FirebaseStorageTask: 183 | if _base_url == "" or Firebase.Auth.auth.is_empty(): 184 | return null 185 | 186 | var task := FirebaseStorageTask.new() 187 | task.ref = ref 188 | task._url = _get_file_url(_root_ref).trim_suffix("/") 189 | task.action = FirebaseStorageTask.Task.TASK_LIST_ALL if list_all else FirebaseStorageTask.Task.TASK_LIST 190 | _process_request(task) 191 | return task 192 | 193 | 194 | func _delete(ref : StorageReference) -> FirebaseStorageTask: 195 | if _base_url == "" or Firebase.Auth.auth.is_empty(): 196 | return null 197 | 198 | var task := FirebaseStorageTask.new() 199 | task.ref = ref 200 | task._url = _get_file_url(ref) 201 | task.action = FirebaseStorageTask.Task.TASK_DELETE 202 | _process_request(task) 203 | return task 204 | 205 | 206 | func _process_request(task : FirebaseStorageTask) -> void: 207 | if requesting: 208 | _pending_tasks.append(task) 209 | return 210 | requesting = true 211 | 212 | var headers = Array(task._headers) 213 | headers.append("Authorization: Bearer " + Firebase.Auth.auth.idtoken) 214 | task._headers = PackedStringArray(headers) 215 | 216 | _current_task = task 217 | _response_code = 0 218 | _response_headers = PackedStringArray() 219 | _response_data = PackedByteArray() 220 | _content_length = 0 221 | _reading_body = false 222 | 223 | if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]: 224 | _http_client.close() 225 | set_process_internal(true) 226 | 227 | 228 | func _finish_request(result : int) -> void: 229 | var task := _current_task 230 | requesting = false 231 | 232 | task.result = result 233 | task.response_code = _response_code 234 | task.response_headers = _response_headers 235 | 236 | match task.action: 237 | FirebaseStorageTask.Task.TASK_DOWNLOAD: 238 | task.data = _response_data 239 | 240 | FirebaseStorageTask.Task.TASK_DELETE: 241 | _references.erase(task.ref.full_path) 242 | task.ref.valid = false 243 | if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: 244 | task.data = null 245 | 246 | FirebaseStorageTask.Task.TASK_DOWNLOAD_URL: 247 | var json : Dictionary = JSON.parse_string(_response_data.get_string_from_utf8()) 248 | if not json.is_empty() and json.has("downloadTokens"): 249 | task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens 250 | else: 251 | task.data = "" 252 | 253 | FirebaseStorageTask.Task.TASK_LIST, FirebaseStorageTask.Task.TASK_LIST_ALL: 254 | var json : Dictionary = JSON.parse_string(_response_data.get_string_from_utf8()) 255 | var items := [] 256 | if not json.is_empty() and "items" in json: 257 | for item in json.items: 258 | var item_name : String = item.name 259 | if item.bucket != bucket: 260 | continue 261 | if not item_name.begins_with(task.ref.full_path): 262 | continue 263 | if task.action == FirebaseStorageTask.Task.TASK_LIST: 264 | var dir_path : Array = item_name.split("/") 265 | var slash_count : int = task.ref.full_path.count("/") 266 | item_name = "" 267 | for i in slash_count + 1: 268 | item_name += dir_path[i] 269 | if i != slash_count and slash_count != 0: 270 | item_name += "/" 271 | if item_name in items: 272 | continue 273 | 274 | items.append(item_name) 275 | task.data = items 276 | 277 | _: 278 | task.data = JSON.parse_string(_response_data.get_string_from_utf8()) 279 | 280 | var next_task : FirebaseStorageTask 281 | if not _pending_tasks.is_empty(): 282 | next_task = _pending_tasks.pop_front() 283 | 284 | task.finished = true 285 | task.task_finished.emit() 286 | if typeof(task.data) == TYPE_DICTIONARY and "error" in task.data: 287 | task_failed.emit(task.result, task.response_code, task.data) 288 | else: 289 | task_successful.emit(task.result, task.response_code, task.data) 290 | 291 | while true: 292 | if next_task and not next_task.finished: 293 | _process_request(next_task) 294 | break 295 | elif not _pending_tasks.is_empty(): 296 | next_task = _pending_tasks.pop_front() 297 | else: 298 | break 299 | 300 | 301 | func _get_file_url(ref : StorageReference) -> String: 302 | var url := _extended_url.replace("[APP_ID]", ref.bucket) 303 | url = url.replace("[API_VERSION]", _API_VERSION) 304 | return url.replace("[FILE_PATH]", ref.full_path.replace("/", "%2F")) 305 | 306 | 307 | # Removes any "../" or "./" in the file path. 308 | func _simplify_path(path : String) -> String: 309 | var dirs := path.split("/") 310 | var new_dirs := [] 311 | for dir in dirs: 312 | if dir == "..": 313 | new_dirs.pop_back() 314 | elif dir == ".": 315 | pass 316 | else: 317 | new_dirs.push_back(dir) 318 | 319 | var new_path := "/".join(PackedStringArray(new_dirs)) 320 | new_path = new_path.replace("//", "/") 321 | new_path = new_path.replace("\\", "/") 322 | return new_path 323 | -------------------------------------------------------------------------------- /addons/FirebaseAPI/auth/auth.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseAuth 3 | extends HTTPRequest 4 | 5 | 6 | const _API_VERSION := "v1" 7 | 8 | # Emitted for each Auth request issued. 9 | # `result_code` -> Either `1` if auth succeeded or `error_code` if unsuccessful auth request 10 | # `result_content` -> Either `auth_result` if auth succeeded or `error_message` if unsuccessful auth request 11 | signal auth_request(result_code, result_content) 12 | 13 | signal signup_succeeded(auth_result) 14 | signal login_succeeded(auth_result) 15 | signal login_failed(code, message) 16 | signal signup_failed(code, message) 17 | signal userdata_received(userdata) 18 | signal token_exchanged(successful) 19 | signal token_refresh_succeeded(auth_result) 20 | signal logged_out() 21 | 22 | const RESPONSE_SIGNUP := "identitytoolkit#SignupNewUserResponse" 23 | const RESPONSE_SIGNIN := "identitytoolkit#VerifyPasswordResponse" 24 | const RESPONSE_ASSERTION := "identitytoolkit#VerifyAssertionResponse" 25 | const RESPONSE_USERDATA := "identitytoolkit#GetAccountInfoResponse" 26 | const RESPONSE_CUSTOM_TOKEN := "identitytoolkit#VerifyCustomTokenResponse" 27 | 28 | var _base_url : String 29 | var _refresh_request_base_url 30 | var _signup_request_url : String 31 | var _signin_with_oauth_request_url : String 32 | var _signin_request_url : String 33 | var _signin_custom_token_url : String 34 | var _userdata_request_url : String 35 | var _oobcode_request_url : String 36 | var _delete_account_request_url : String 37 | var _update_account_request_url : String 38 | 39 | var _refresh_request_url : String 40 | var _google_auth_request_url := "https://accounts.google.com/o/oauth2/v2/auth?" 41 | var _google_token_request_url := "https://oauth2.googleapis.com/token?" 42 | 43 | var auth := {} 44 | var _needs_refresh := false 45 | var is_busy := false 46 | 47 | var tcp_server : TCPServer = TCPServer.new() 48 | var tcp_timer : Timer = Timer.new() 49 | var tcp_timeout : float = 0.5 50 | 51 | var _headers := [ 52 | "Accept: application/json", 53 | "Content-Type: application/json" 54 | ] 55 | 56 | var requesting := -1 57 | 58 | enum Requests { 59 | NONE = -1, 60 | EXCHANGE_TOKEN, 61 | LOGIN_WITH_OAUTH 62 | } 63 | 64 | var auth_request_type := -1 65 | 66 | enum Auth_Type { 67 | NONE = -1, 68 | LOGIN_EP, 69 | LOGIN_ANON, 70 | LOGIN_CT, 71 | LOGIN_OAUTH, 72 | SIGNUP_EP 73 | } 74 | 75 | var _login_request_body := { 76 | "email":"", 77 | "password":"", 78 | "returnSecureToken": true, 79 | } 80 | 81 | var _post_body := "id_token=[GOOGLE_ID_TOKEN]&providerId=[PROVIDER_ID]" 82 | var _request_uri := "[REQUEST_URI]" 83 | 84 | var _oauth_login_request_body := { 85 | "postBody":"", 86 | "requestUri":"", 87 | "returnIdpCredential":true, 88 | "returnSecureToken":true 89 | } 90 | 91 | var _anonymous_login_request_body := { 92 | "returnSecureToken":true 93 | } 94 | 95 | var _refresh_request_body := { 96 | "grant_type":"refresh_token", 97 | "refresh_token":"", 98 | } 99 | 100 | var _custom_token_body := { 101 | "token":"", 102 | "returnSecureToken":true 103 | } 104 | 105 | var _password_reset_body := { 106 | "requestType":"password_reset", 107 | "email":"", 108 | } 109 | 110 | var _change_email_body := { 111 | "idToken":"", 112 | "email":"", 113 | "returnSecureToken": true, 114 | } 115 | 116 | var _change_password_body := { 117 | "idToken":"", 118 | "password":"", 119 | "returnSecureToken": true, 120 | } 121 | 122 | var _account_verification_body := { 123 | "requestType":"verify_email", 124 | "idToken":"", 125 | } 126 | 127 | var _update_profile_body := { 128 | "idToken":"", 129 | "displayName":"", 130 | "photoUrl":"", 131 | "deleteAttribute":"", 132 | "returnSecureToken":true 133 | } 134 | 135 | var _google_auth_body := { 136 | "scope":"email openid profile", 137 | "response_type":"code", 138 | "redirect_uri":"", 139 | "client_id":"[CLIENT_ID]" 140 | } 141 | 142 | var _google_token_body := { 143 | "code":"", 144 | "client_id":"", 145 | "client_secret":"", 146 | "redirect_uri":"", 147 | "grant_type":"authorization_code" 148 | } 149 | 150 | 151 | func _ready() -> void: 152 | tcp_timer.wait_time = tcp_timeout 153 | tcp_timer.timeout.connect(_tcp_stream_timer) 154 | connect("request_completed",_on_FirebaseAuth_request_completed) 155 | 156 | 157 | func _setup(config_json : Dictionary) -> void: 158 | _signup_request_url = "accounts:signUp?key=" + config_json.apiKey 159 | _signin_request_url = "accounts:signInWithPassword?key=" + config_json.apiKey 160 | _signin_custom_token_url = "accounts:signInWithCustomToken?key=" + config_json.apiKey 161 | _signin_with_oauth_request_url = "accounts:signInWithIdp?key=" + config_json.apiKey 162 | _userdata_request_url = "accounts:lookup?key=" + config_json.apiKey 163 | _refresh_request_url = "/v1/token?key=" + config_json.apiKey 164 | _oobcode_request_url = "accounts:sendOobCode?key=" + config_json.apiKey 165 | _delete_account_request_url = "accounts:delete?key=" + config_json.apiKey 166 | _update_account_request_url = "accounts:update?key=" + config_json.apiKey 167 | _base_url = "https://identitytoolkit.googleapis.com/" + _API_VERSION + "/" 168 | _refresh_request_base_url = "https://securetoken.googleapis.com" 169 | 170 | 171 | func _is_ready() -> bool: 172 | if is_busy: 173 | Firebase._printerr("Firebase Auth is currently busy and cannot process this request") 174 | return false 175 | else: 176 | if _base_url == "": 177 | Firebase._printerr("Firebase hasn't been configured") 178 | return false 179 | return true 180 | 181 | 182 | # Synchronous call to check if any user is already logged in. 183 | func is_logged_in() -> bool: 184 | return auth != null and auth.has("idtoken") 185 | 186 | 187 | # Called with Firebase.Auth.signup_with_email_and_password(email, password) 188 | # You must pass in the email and password to this function for it to work correctly 189 | func signup_with_email_and_password(email : String, password : String) -> void: 190 | if _is_ready(): 191 | is_busy = true 192 | _login_request_body.email = email 193 | _login_request_body.password = password 194 | auth_request_type = Auth_Type.SIGNUP_EP 195 | request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) 196 | 197 | 198 | # Called with Firebase.Auth.anonymous_login() 199 | # A successful request is indicated by a 200 OK HTTP status code. 200 | # The response contains the Firebase ID token and refresh token associated with the anonymous user. 201 | # The 'mail' field will be empty since no email is linked to an anonymous user 202 | func login_anonymous() -> void: 203 | if _is_ready(): 204 | is_busy = true 205 | auth_request_type = Auth_Type.LOGIN_ANON 206 | request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_anonymous_login_request_body)) 207 | 208 | 209 | func login_with_email_and_password(email : String, password : String) -> void: 210 | if _is_ready(): 211 | is_busy = true 212 | _login_request_body.email = email 213 | _login_request_body.password = password 214 | auth_request_type = Auth_Type.LOGIN_EP 215 | request(_base_url + _signin_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) 216 | 217 | 218 | # The token needs to be generated using an external service/function 219 | func login_with_custom_token(token : String) -> void: 220 | if _is_ready(): 221 | is_busy = true 222 | _custom_token_body.token = token 223 | auth_request_type = Auth_Type.LOGIN_CT 224 | request(_base_url + _signin_custom_token_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_custom_token_body)) 225 | 226 | # Open a web page in browser redirecting to Google oAuth2 page for the current project 227 | # Once given user's authorization, a token will be generated. 228 | # NOTE** with this method, the authorization process will be copy-pasted 229 | 230 | func get_google_auth(redirect_uri : String = "urn:ietf:wg:oauth:2.0:oob", client_id : String = Firebase._config.clientId) -> void: 231 | var url_endpoint : String = _google_auth_request_url 232 | _google_auth_body.redirect_uri = redirect_uri 233 | 234 | 235 | func get_google_auth_manual() -> void: 236 | var url_endpoint : String = _google_auth_request_url 237 | _google_auth_body.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" 238 | for key in _google_auth_body.keys(): 239 | url_endpoint += key + "=" + _google_auth_body[key] + "&" 240 | url_endpoint = url_endpoint.replace("[CLIENT_ID]&", Firebase._config.clientId) 241 | OS.shell_open(url_endpoint) 242 | 243 | # Exchange the authorization oAuth2 code obtained from browser with a proper access id_token 244 | func exchange_google_token(google_token : String, redirect_uri : String = "urn:ietf:wg:oauth:2.0:oob") -> void: 245 | if _is_ready(): 246 | is_busy = true 247 | _google_token_body.code = google_token 248 | _google_token_body.client_id = Firebase._config.clientId 249 | _google_token_body.client_secret = Firebase._config.clientSecret 250 | _google_token_body.redirect_uri = redirect_uri 251 | requesting = Requests.EXCHANGE_TOKEN 252 | request(_google_token_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_google_token_body)) 253 | 254 | 255 | func get_google_auth_redirect(redirect_uri : String, listen_to_port : int) -> void: 256 | var url_endpoint : String = _google_auth_request_url 257 | _google_auth_body.redirect_uri = redirect_uri 258 | for key in _google_auth_body.keys(): 259 | url_endpoint += key + "=" + _google_auth_body[key] + "&" 260 | url_endpoint = url_endpoint.replace("[CLIENT_ID]&", Firebase._config.clientId) 261 | OS.shell_open(url_endpoint) 262 | await get_tree().create_timer(1).timeout 263 | add_child(tcp_timer) 264 | tcp_timer.start() 265 | tcp_server.listen(listen_to_port, "::") 266 | 267 | 268 | # Open a web page in browser redirecting to Google oAuth2 page for the current project 269 | # Once given user's authorization, a token will be generated. 270 | # NOTE** the generated token will be automatically captured and a login request will be made if the token is correct 271 | func get_google_auth_localhost(port : int = 49152): 272 | get_google_auth_redirect("http://localhost:%s/" % port, port) 273 | 274 | 275 | func _tcp_stream_timer() -> void: 276 | var peer : StreamPeer = tcp_server.take_connection() 277 | if peer != null: 278 | var raw_result : String = peer.get_utf8_string(100) 279 | if raw_result != "" and raw_result.begins_with("GET"): 280 | var token : String = raw_result.rsplit("=")[1].rstrip("&scope") 281 | tcp_server.stop() 282 | peer.disconnect_from_host() 283 | tcp_timer.stop() 284 | remove_child(tcp_timer) 285 | login_with_oauth(token, _google_auth_body.redirect_uri) 286 | 287 | 288 | # A token is automatically obtained using an authorization code using @get_google_auth() 289 | func login_with_oauth(_google_token: String, request_uri : String = "urn:ietf:wg:oauth:2.0:oob", provider_id : String = "google.com") -> void: 290 | var google_token : String = _google_token.uri_decode() 291 | _exchange_google_token(google_token, request_uri) 292 | var is_successful : bool = await token_exchanged 293 | if is_successful and _is_ready(): 294 | is_busy = true 295 | _oauth_login_request_body.postBody = _post_body.replace("[GOOGLE_ID_TOKEN]", auth.idtoken).replace("[PROVIDER_ID]", provider_id) 296 | _oauth_login_request_body.requestUri = _request_uri.replace("[REQUEST_URI]", request_uri if request_uri != "urn:ietf:wg:oauth:2.0:oob" else "http://localhost") 297 | requesting = Requests.LOGIN_WITH_OAUTH 298 | auth_request_type = Auth_Type.LOGIN_OAUTH 299 | request(_base_url + _signin_with_oauth_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_oauth_login_request_body)) 300 | 301 | 302 | func _exchange_google_token(google_token : String, redirect_uri : String = "urn:ietf:wg:oauth:2.0:oob") -> void: 303 | if _is_ready(): 304 | is_busy = true 305 | _google_token_body.code = google_token 306 | _google_token_body.redirect_uri = redirect_uri 307 | _google_token_body.client_id = Firebase._config.clientId 308 | _google_token_body.client_secret = Firebase._config.clientSecret 309 | requesting = Requests.EXCHANGE_TOKEN 310 | request(_google_token_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_google_token_body)) 311 | 312 | 313 | func manual_token_refresh(auth_data): 314 | auth = auth_data 315 | var refresh_token = null 316 | auth = get_clean_keys(auth) 317 | if auth.has("refreshtoken"): 318 | refresh_token = auth.refreshtoken 319 | elif auth.has("refresh_token"): 320 | refresh_token = auth.refresh_token 321 | _needs_refresh = true 322 | _refresh_request_body.refresh_token = refresh_token 323 | request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) 324 | 325 | 326 | # This function is called whenever there is an authentication request to Firebase 327 | # On an error, this function with emit the signal 'login_failed' and print the error to the console 328 | func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 329 | is_busy = false 330 | var res 331 | if response_code == 0: 332 | # Mocked error results to trigger the correct signal. 333 | # Can occur if there is no internet connection, or the service is down, 334 | # in which case there is no json_body (and thus parsing would fail). 335 | res = {"error": { 336 | "code": "Connection error", 337 | "message": "Error connecting to auth service"}} 338 | else: 339 | var bod = body.get_string_from_utf8() 340 | var json_result = JSON.parse_string(bod) 341 | if json_result == null: 342 | Firebase._printerr("Error while parsing auth body json") 343 | auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json") 344 | return 345 | res = json_result 346 | 347 | if response_code == HTTPClient.RESPONSE_OK: 348 | if not res.has("kind"): 349 | auth = get_clean_keys(res) 350 | match requesting: 351 | Requests.EXCHANGE_TOKEN: 352 | token_exchanged.emit(true) 353 | begin_refresh_countdown() 354 | # Refresh token countdown 355 | auth_request.emit(1, auth) 356 | else: 357 | match res.kind: 358 | RESPONSE_SIGNUP: 359 | auth = get_clean_keys(res) 360 | signup_succeeded.emit(auth) 361 | begin_refresh_countdown() 362 | RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: 363 | auth = get_clean_keys(res) 364 | login_succeeded.emit(auth) 365 | begin_refresh_countdown() 366 | RESPONSE_USERDATA: 367 | var userdata = FirebaseUserData.new(res.users[0]) 368 | userdata_received.emit(userdata) 369 | auth_request.emit(1, auth) 370 | else: 371 | # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD 372 | if requesting == Requests.EXCHANGE_TOKEN: 373 | token_exchanged.emit(false) 374 | login_failed.emit(res.error, res.error_description) 375 | auth_request.emit(res.error, res.error_description) 376 | else: 377 | if auth_request_type == Auth_Type.SIGNUP_EP: 378 | signup_failed.emit(res.error.code, res.error.message) 379 | else: 380 | login_failed.emit(res.error.code, res.error.message) 381 | auth_request.emit(res.error.code, res.error.message) 382 | requesting = Requests.NONE 383 | auth_request_type = Auth_Type.NONE 384 | 385 | 386 | 387 | # Function used to change the email account for the currently logged in user 388 | func change_user_email(email : String) -> void: 389 | if _is_ready(): 390 | is_busy = true 391 | _change_email_body.email = email 392 | _change_email_body.idToken = auth.idtoken 393 | request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_email_body)) 394 | 395 | 396 | # Function used to change the password for the currently logged in user 397 | func change_user_password(password : String) -> void: 398 | if _is_ready(): 399 | is_busy = true 400 | _change_password_body.password = password 401 | _change_password_body.idToken = auth.idtoken 402 | request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_password_body)) 403 | 404 | 405 | # User Profile handlers 406 | func update_account(idToken : String, displayName : String, photoUrl : String, deleteAttribute : PackedStringArray, returnSecureToken : bool) -> void: 407 | if _is_ready(): 408 | is_busy = true 409 | _update_profile_body.idToken = idToken 410 | _update_profile_body.displayName = displayName 411 | _update_profile_body.photoUrl = photoUrl 412 | _update_profile_body.deleteAttribute = deleteAttribute 413 | _update_profile_body.returnSecureToken = returnSecureToken 414 | request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_update_profile_body)) 415 | 416 | 417 | # Function to send a account verification email 418 | func send_account_verification_email() -> void: 419 | if _is_ready(): 420 | is_busy = true 421 | _account_verification_body.idToken = auth.idtoken 422 | request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_account_verification_body)) 423 | 424 | 425 | # Function used to reset the password for a user who has forgotten in. 426 | # This will send the users account an email with a password reset link 427 | func send_password_reset_email(email : String) -> void: 428 | if _is_ready(): 429 | is_busy = true 430 | _password_reset_body.email = email 431 | request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_password_reset_body)) 432 | 433 | 434 | # Function called to get all 435 | func get_user_data() -> void: 436 | if _is_ready(): 437 | is_busy = true 438 | if not is_logged_in(): 439 | print_debug("Not logged in") 440 | is_busy = false 441 | return 442 | 443 | request(_base_url + _userdata_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) 444 | 445 | 446 | # Function used to delete the account of the currently authenticated user 447 | func delete_user_account() -> void: 448 | if _is_ready(): 449 | is_busy = true 450 | request(_base_url + _delete_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) 451 | 452 | 453 | # 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. 454 | func begin_refresh_countdown() -> void: 455 | var refresh_token = null 456 | var expires_in = 1000 457 | auth = get_clean_keys(auth) 458 | if auth.has("refreshtoken"): 459 | refresh_token = auth.refreshtoken 460 | expires_in = auth.expiresin 461 | elif auth.has("refresh_token"): 462 | refresh_token = auth.refresh_token 463 | expires_in = auth.expires_in 464 | if auth.has("userid"): 465 | auth.localid = auth.userid 466 | _needs_refresh = true 467 | token_refresh_succeeded.emit(auth) 468 | await get_tree().create_timer(float(expires_in)).timeout 469 | _refresh_request_body.refresh_token = refresh_token 470 | request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) 471 | 472 | 473 | # This function is used to make all keys lowercase 474 | # This is only used to cut down on processing errors from Firebase 475 | func get_clean_keys(auth_result : Dictionary) -> Dictionary: 476 | var cleaned : Dictionary 477 | for key in auth_result.keys(): 478 | cleaned[key.replace("_", "").to_lower()] = auth_result[key] 479 | return cleaned 480 | --------------------------------------------------------------------------------