├── 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 |
--------------------------------------------------------------------------------