added firebase and rudimentary leaderboard support

This commit is contained in:
derek
2025-04-09 11:19:02 -05:00
parent ce08df66e6
commit 25eb9e725a
121 changed files with 4987 additions and 4 deletions

View File

@@ -0,0 +1,24 @@
[firebase/environment_variables]
"apiKey"="AIzaSyB9_oTvQOZp1XaqErfmrl2A0K1bm9G2_S4",
"authDomain"="godotfps.firebaseapp.com",
"databaseURL"="",
"projectId"="godotfps",
"storageBucket"="godotfps.firebasestorage.app",
"messagingSenderId"="703030364789",
"appId"="1:703030364789:web:dab5431813950942a2c63c",
"measurementId"="G-XD7BKEVQL5"
"clientId"=""
"clientSecret"=""
"domainUriPrefix"=""
"functionsGeoZone"=""
"cacheLocation"=""
[firebase/emulators/ports]
authentication=""
firestore=""
realtimeDatabase=""
functions=""
storage=""
dynamicLinks=""

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Kyle Szklenski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,3 @@
# Godot Firebase
A Google Firebase SDK written in GDScript for use in Godot Engine projects. For more information about usage, support, and contribution, check out the [GitHub Repository](https://github.com/WolfgangSenff/GodotFirebase) and the [Wiki](https://github.com/WolfgangSenff/GodotFirebase/wiki).

View File

@@ -0,0 +1,346 @@
extends Node
class_name Utilities
static func get_json_data(value):
if value is PackedByteArray:
value = value.get_string_from_utf8()
var json = JSON.new()
var json_parse_result = json.parse(value)
if json_parse_result == OK:
return json.data
return null
# Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields
# Field Path3D using the "dot" (`.`) notation are supported:
# ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } }
static func dict2fields(dict : Dictionary) -> Dictionary:
var fields = {}
var var_type : String = ""
for field in dict.keys():
var field_value = dict[field]
if field is String and "." in field:
var keys: Array = field.split(".")
field = keys.pop_front()
keys.reverse()
for key in keys:
field_value = { key : field_value }
match typeof(field_value):
TYPE_NIL: var_type = "nullValue"
TYPE_BOOL: var_type = "booleanValue"
TYPE_INT: var_type = "integerValue"
TYPE_FLOAT: var_type = "doubleValue"
TYPE_STRING: var_type = "stringValue"
TYPE_DICTIONARY:
if is_field_timestamp(field_value):
var_type = "timestampValue"
field_value = dict2timestamp(field_value)
else:
var_type = "mapValue"
field_value = dict2fields(field_value)
TYPE_ARRAY:
var_type = "arrayValue"
field_value = {"values": array2fields(field_value)}
if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"):
for key in field_value["fields"].keys():
fields[field]["mapValue"]["fields"][key] = field_value["fields"][key]
else:
fields[field] = { var_type : field_value }
return {'fields' : fields}
class FirebaseTypeConverter extends RefCounted:
var converters = {
"nullValue": _to_null,
"booleanValue": _to_bool,
"integerValue": _to_int,
"doubleValue": _to_float
}
func convert_value(type, value):
if converters.has(type):
return converters[type].call(value)
return value
func _to_null(value):
return null
func _to_bool(value):
return bool(value)
func _to_int(value):
return int(value)
func _to_float(value):
return float(value)
static func from_firebase_type(value):
if value == null:
return null
if value.has("mapValue"):
value = fields2dict(value.values()[0])
elif value.has("arrayValue"):
value = fields2array(value.values()[0])
elif value.has("timestampValue"):
value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false)
else:
var converter = FirebaseTypeConverter.new()
value = converter.convert_value(value.keys()[0], value.values()[0])
return value
static func to_firebase_type(value : Variant) -> Dictionary:
var var_type : String = ""
match typeof(value):
TYPE_NIL: var_type = "nullValue"
TYPE_BOOL: var_type = "booleanValue"
TYPE_INT: var_type = "integerValue"
TYPE_FLOAT: var_type = "doubleValue"
TYPE_STRING: var_type = "stringValue"
TYPE_DICTIONARY:
if is_field_timestamp(value):
var_type = "timestampValue"
value = dict2timestamp(value)
else:
var_type = "mapValue"
value = dict2fields(value)
TYPE_ARRAY:
var_type = "arrayValue"
value = {"values": array2fields(value)}
return { var_type : value }
# Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' }
static func fields2dict(doc) -> Dictionary:
var dict = {}
if doc.has("fields"):
var fields = doc["fields"]
for field in fields.keys():
if fields[field].has("mapValue"):
dict[field] = (fields2dict(fields[field].mapValue))
elif fields[field].has("timestampValue"):
dict[field] = timestamp2dict(fields[field].timestampValue)
elif fields[field].has("arrayValue"):
dict[field] = fields2array(fields[field].arrayValue)
elif fields[field].has("integerValue"):
dict[field] = fields[field].values()[0] as int
elif fields[field].has("doubleValue"):
dict[field] = fields[field].values()[0] as float
elif fields[field].has("booleanValue"):
dict[field] = fields[field].values()[0] as bool
elif fields[field].has("nullValue"):
dict[field] = null
else:
dict[field] = fields[field].values()[0]
return dict
# Pass an Array to parse it to a Firebase arrayValue
static func array2fields(array : Array) -> Array:
var fields : Array = []
var var_type : String = ""
for field in array:
match typeof(field):
TYPE_DICTIONARY:
if is_field_timestamp(field):
var_type = "timestampValue"
field = dict2timestamp(field)
else:
var_type = "mapValue"
field = dict2fields(field)
TYPE_NIL: var_type = "nullValue"
TYPE_BOOL: var_type = "booleanValue"
TYPE_INT: var_type = "integerValue"
TYPE_FLOAT: var_type = "doubleValue"
TYPE_STRING: var_type = "stringValue"
TYPE_ARRAY: var_type = "arrayValue"
_: var_type = "FieldTransform"
fields.append({ var_type : field })
return fields
# Pass a Firebase arrayValue Dictionary to convert it back to an Array
static func fields2array(array : Dictionary) -> Array:
var fields : Array = []
if array.has("values"):
for field in array.values:
var item
match field.keys()[0]:
"mapValue":
item = fields2dict(field.mapValue)
"arrayValue":
item = fields2array(field.arrayValue)
"integerValue":
item = field.values()[0] as int
"doubleValue":
item = field.values()[0] as float
"booleanValue":
item = field.values()[0] as bool
"timestampValue":
item = timestamp2dict(field.timestampValue)
"nullValue":
item = null
_:
item = field.values()[0]
fields.append(item)
return fields
# Converts a gdscript Dictionary (most likely obtained with Time.get_datetime_dict_from_system()) to a Firebase Timestamp
static func dict2timestamp(dict : Dictionary) -> String:
#dict.erase('weekday')
#dict.erase('dst')
#var dict_values : Array = dict.values()
var time = Time.get_datetime_string_from_datetime_dict(dict, false)
return time
#return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values
# Converts a Firebase Timestamp back to a gdscript Dictionary
static func timestamp2dict(timestamp : String) -> Dictionary:
return Time.get_datetime_dict_from_datetime_string(timestamp, false)
#var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0}
#var dict : PackedStringArray = timestamp.split("T")[0].split("-")
#dict.append_array(timestamp.split("T")[1].split(":"))
#for value in dict.size():
#datetime[datetime.keys()[value]] = int(dict[value])
#return datetime
static func is_field_timestamp(field : Dictionary) -> bool:
return field.has_all(['year','month','day','hour','minute','second'])
# HTTPRequeust seems to have an issue in Web exports where the body returns empty
# This appears to be caused by the gzip compression being unsupported, so we
# disable it when web export is detected.
static func fix_http_request(http_request):
if is_web():
http_request.accept_gzip = false
static func is_web() -> bool:
return OS.get_name() in ["HTML5", "Web"]
class MultiSignal extends RefCounted:
signal completed(with_signal)
signal all_completed()
var _has_signaled := false
var _early_exit := false
var signal_count := 0
func _init(sigs : Array[Signal], early_exit := true, should_oneshot := true) -> void:
_early_exit = early_exit
for sig in sigs:
add_signal(sig, should_oneshot)
func add_signal(sig : Signal, should_oneshot) -> void:
signal_count += 1
sig.connect(
func():
if not _has_signaled and _early_exit:
completed.emit(sig)
_has_signaled = true
elif not _early_exit:
completed.emit(sig)
signal_count -= 1
if signal_count <= 0: # Not sure how it could be less than
all_completed.emit()
, CONNECT_ONE_SHOT if should_oneshot else CONNECT_REFERENCE_COUNTED
)
class SignalReducer extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object.
signal completed
var awaiters : Array[Signal] = []
var reducers = {
0 : func(): completed.emit(),
1 : func(p): completed.emit(),
2 : func(p1, p2): completed.emit(),
3 : func(p1, p2, p3): completed.emit(),
4 : func(p1, p2, p3, p4): completed.emit()
}
func add_signal(sig : Signal, param_count : int = 0) -> void:
assert(param_count < 5, "Too many parameters to reduce, just add more!")
sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing
class SignalReducerWithResult extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object.
signal completed(result)
var awaiters : Array[Signal] = []
var reducers = {
0 : func(): completed.emit(),
1 : func(p): completed.emit({1 : p}),
2 : func(p1, p2): completed.emit({ 1 : p1, 2 : p2 }),
3 : func(p1, p2, p3): completed.emit({ 1 : p1, 2 : p2, 3 : p3 }),
4 : func(p1, p2, p3, p4): completed.emit({ 1 : p1, 2 : p2, 3 : p3, 4 : p4 })
}
func add_signal(sig : Signal, param_count : int = 0) -> void:
assert(param_count < 5, "Too many parameters to reduce, just add more!")
sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing
class ObservableDictionary extends RefCounted:
signal keys_changed()
var _internal : Dictionary
var is_notifying := true
func _init(copy : Dictionary = {}) -> void:
_internal = copy
func add(key : Variant, value : Variant) -> void:
_internal[key] = value
if is_notifying:
keys_changed.emit()
func update(key : Variant, value : Variant) -> void:
_internal[key] = value
if is_notifying:
keys_changed.emit()
func has(key : Variant) -> bool:
return _internal.has(key)
func keys():
return _internal.keys()
func values():
return _internal.values()
func erase(key : Variant) -> bool:
var result = _internal.erase(key)
if is_notifying:
keys_changed.emit()
return result
func get_value(key : Variant) -> Variant:
return _internal[key]
func _get(property: StringName) -> Variant:
if _internal.has(property):
return _internal[property]
return false
func _set(property: StringName, value: Variant) -> bool:
update(property, value)
return true
class AwaitDetachable extends Node2D:
var awaiter : Signal
func _init(freeable_node, await_signal : Signal) -> void:
awaiter = await_signal
add_child(freeable_node)
awaiter.connect(queue_free)

View File

@@ -0,0 +1 @@
uid://cggx7ysauq6p0

View File

@@ -0,0 +1,693 @@
## @meta-authors TODO
## @meta-version 2.5
## The authentication API for Firebase.
## Documentation TODO.
@tool
class_name FirebaseAuth
extends HTTPRequest
const _API_VERSION : String = "v1"
const _INAPP_PLUGIN : String = "GodotSvc"
# Emitted for each Auth request issued.
# `result_code` -> Either `1` if auth succeeded or `error_code` if unsuccessful auth request
# `result_content` -> Either `auth_result` if auth succeeded or `error_message` if unsuccessful auth request
signal auth_request(result_code, result_content)
signal signup_succeeded(auth_result)
signal login_succeeded(auth_result)
signal login_failed(code, message)
signal signup_failed(code, message)
signal userdata_received(userdata)
signal token_exchanged(successful)
signal token_refresh_succeeded(auth_result)
signal logged_out()
const RESPONSE_SIGNUP : String = "identitytoolkit#SignupNewUserResponse"
const RESPONSE_SIGNIN : String = "identitytoolkit#VerifyPasswordResponse"
const RESPONSE_ASSERTION : String = "identitytoolkit#VerifyAssertionResponse"
const RESPONSE_USERDATA : String = "identitytoolkit#GetAccountInfoResponse"
const RESPONSE_CUSTOM_TOKEN : String = "identitytoolkit#VerifyCustomTokenResponse"
var _base_url : String = ""
var _refresh_request_base_url = ""
var _signup_request_url : String = "accounts:signUp?key=%s"
var _signin_with_oauth_request_url : String = "accounts:signInWithIdp?key=%s"
var _signin_request_url : String = "accounts:signInWithPassword?key=%s"
var _signin_custom_token_url : String = "accounts:signInWithCustomToken?key=%s"
var _userdata_request_url : String = "accounts:lookup?key=%s"
var _oobcode_request_url : String = "accounts:sendOobCode?key=%s"
var _delete_account_request_url : String = "accounts:delete?key=%s"
var _update_account_request_url : String = "accounts:update?key=%s"
var _refresh_request_url : String = "/v1/token?key=%s"
var _google_auth_request_url : String = "https://accounts.google.com/o/oauth2/v2/auth?"
var _config : Dictionary = {}
var auth : Dictionary = {}
var _needs_refresh : bool = false
var is_busy : bool = false
var has_child : bool = false
var is_oauth_login: bool = false
var tcp_server : TCPServer = TCPServer.new()
var tcp_timer : Timer = Timer.new()
var tcp_timeout : float = 0.5
var _headers : PackedStringArray = [
"Content-Type: application/json",
"Accept: application/json",
]
var requesting : int = -1
enum Requests {
NONE = -1,
EXCHANGE_TOKEN,
LOGIN_WITH_OAUTH
}
var auth_request_type : int = -1
enum Auth_Type {
NONE = -1,
LOGIN_EP,
LOGIN_ANON,
LOGIN_CT,
LOGIN_OAUTH,
SIGNUP_EP
}
var _login_request_body : Dictionary = {
"email":"",
"password":"",
"returnSecureToken": true,
}
var _oauth_login_request_body : Dictionary = {
"postBody":"",
"requestUri":"",
"returnIdpCredential":false,
"returnSecureToken":true
}
var _anonymous_login_request_body : Dictionary = {
"returnSecureToken":true
}
var _refresh_request_body : Dictionary = {
"grant_type":"refresh_token",
"refresh_token":"",
}
var _custom_token_body : Dictionary = {
"token":"",
"returnSecureToken":true
}
var _password_reset_body : Dictionary = {
"requestType":"password_reset",
"email":"",
}
var _change_email_body : Dictionary = {
"idToken":"",
"email":"",
"returnSecureToken": true,
}
var _change_password_body : Dictionary = {
"idToken":"",
"password":"",
"returnSecureToken": true,
}
var _account_verification_body : Dictionary = {
"requestType":"verify_email",
"idToken":"",
}
var _update_profile_body : Dictionary = {
"idToken":"",
"displayName":"",
"photoUrl":"",
"deleteAttribute":"",
"returnSecureToken":true
}
var link_account_body : Dictionary = {
"idToken":"",
"email":"",
"password":"",
"returnSecureToken":true
}
var _local_port : int = 8060
var _local_uri : String = "http://localhost:%s/"%_local_port
var _local_provider : AuthProvider = AuthProvider.new()
func _ready() -> void:
tcp_timer.wait_time = tcp_timeout
tcp_timer.timeout.connect(_tcp_stream_timer)
Utilities.fix_http_request(self)
if Utilities.is_web():
_local_uri += "tmp_js_export.html"
# Sets the configuration needed for the plugin to talk to Firebase
# These settings come from the Firebase.gd script automatically
func _set_config(config_json : Dictionary) -> void:
_config = config_json
_signup_request_url %= _config.apiKey
_signin_request_url %= _config.apiKey
_signin_custom_token_url %= _config.apiKey
_signin_with_oauth_request_url %= _config.apiKey
_userdata_request_url %= _config.apiKey
_refresh_request_url %= _config.apiKey
_oobcode_request_url %= _config.apiKey
_delete_account_request_url %= _config.apiKey
_update_account_request_url %= _config.apiKey
request_completed.connect(_on_FirebaseAuth_request_completed)
_check_emulating()
func _check_emulating() -> void :
## Check emulating
if not Firebase.emulating:
_base_url = "https://identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION })
_refresh_request_base_url = "https://securetoken.googleapis.com"
else:
var port : String = _config.emulators.ports.authentication
if port == "":
Firebase._printerr("You are in 'emulated' mode, but the port for Authentication has not been configured.")
else:
_base_url = "http://localhost:{port}/identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION ,port = port })
_refresh_request_base_url = "http://localhost:{port}/securetoken.googleapis.com".format({port = port})
# Function is used to check if the auth script is ready to process a request. Returns true if it is not currently processing
# If false it will print an error
func _is_ready() -> bool:
if is_busy:
Firebase._printerr("Firebase Auth is currently busy and cannot process this request")
return false
else:
return true
# Function cleans the URI and replaces spaces with %20
# As of right now we only replace spaces
# We may need to decide to use the uri_encode() String function
func _clean_url(_url):
_url = _url.replace(' ','%20')
return _url
# Synchronous call to check if any user is already logged in.
func is_logged_in() -> bool:
return auth != null and auth.has("idtoken")
# Called with Firebase.Auth.signup_with_email_and_password(email, password)
# You must pass in the email and password to this function for it to work correctly
func signup_with_email_and_password(email : String, password : String) -> void:
if _is_ready():
is_busy = true
_login_request_body.email = email
_login_request_body.password = password
auth_request_type = Auth_Type.SIGNUP_EP
var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body))
_login_request_body.email = ""
_login_request_body.password = ""
if err != OK:
is_busy = false
Firebase._printerr("Error signing up with password and email: %s" % err)
# Called with Firebase.Auth.anonymous_login()
# A successful request is indicated by a 200 OK HTTP status code.
# The response contains the Firebase ID token and refresh token associated with the anonymous user.
# The 'mail' field will be empty since no email is linked to an anonymous user
func login_anonymous() -> void:
if _is_ready():
is_busy = true
auth_request_type = Auth_Type.LOGIN_ANON
var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_anonymous_login_request_body))
if err != OK:
is_busy = false
Firebase._printerr("Error logging in as anonymous: %s" % err)
# Called with Firebase.Auth.login_with_email_and_password(email, password)
# You must pass in the email and password to this function for it to work correctly
# If the login fails it will return an error code through the function _on_FirebaseAuth_request_completed
func login_with_email_and_password(email : String, password : String) -> void:
if _is_ready():
is_busy = true
_login_request_body.email = email
_login_request_body.password = password
auth_request_type = Auth_Type.LOGIN_EP
var err = request(_base_url + _signin_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body))
_login_request_body.email = ""
_login_request_body.password = ""
if err != OK:
is_busy = false
Firebase._printerr("Error logging in with password and email: %s" % err)
# Login with a custom valid token
# The token needs to be generated using an external service/function
func login_with_custom_token(token : String) -> void:
if _is_ready():
is_busy = true
_custom_token_body.token = token
auth_request_type = Auth_Type.LOGIN_CT
var err = request(_base_url + _signin_custom_token_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_custom_token_body))
if err != OK:
is_busy = false
Firebase._printerr("Error logging in with custom token: %s" % err)
# Open a web page in browser redirecting to Google oAuth2 page for the current project
# Once given user's authorization, a token will be generated.
# NOTE** the generated token will be automatically captured and a login request will be made if the token is correct
func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port : int = _local_port):
get_auth_with_redirect(provider)
await get_tree().create_timer(0.5).timeout
if has_child == false:
add_child(tcp_timer)
has_child = true
tcp_timer.start()
tcp_server.listen(port, "*")
func get_auth_with_redirect(provider: AuthProvider) -> void:
var url_endpoint: String = provider.redirect_uri
for key in provider.params.keys():
url_endpoint+=key+"="+provider.params[key]+"&"
url_endpoint += provider.params.redirect_type+"="+_local_uri
url_endpoint = _clean_url(url_endpoint)
if Utilities.is_web() and OS.has_feature("JavaScript"):
JavaScriptBridge.eval('window.location.replace("' + url_endpoint + '")')
elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS":
#in app for ios if the iOS plugin exists
set_local_provider(provider)
Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint)
else:
set_local_provider(provider)
OS.shell_open(url_endpoint)
# Login with Google oAuth2.
# A token is automatically obtained using an authorization code using @get_google_auth()
# @provider_id and @request_uri can be changed
func login_with_oauth(_token: String, provider: AuthProvider) -> void:
if _token:
is_oauth_login = true
var token : String = _token.uri_decode()
var is_successful: bool = true
if provider.should_exchange:
exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret())
is_successful = await self.token_exchanged
token = auth.accesstoken
if is_successful and _is_ready():
is_busy = true
_oauth_login_request_body.postBody = "access_token="+token+"&providerId="+provider.provider_id
_oauth_login_request_body.requestUri = _local_uri
requesting = Requests.LOGIN_WITH_OAUTH
auth_request_type = Auth_Type.LOGIN_OAUTH
var err = request(_base_url + _signin_with_oauth_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_oauth_login_request_body))
_oauth_login_request_body.postBody = ""
_oauth_login_request_body.requestUri = ""
if err != OK:
is_busy = false
Firebase._printerr("Error logging in with oauth: %s" % err)
# Exchange the authorization oAuth2 code obtained from browser with a proper access id_token
func exchange_token(code : String, redirect_uri : String, request_url: String, _client_id: String, _client_secret: String) -> void:
if _is_ready():
is_busy = true
var exchange_token_body : Dictionary = {
code = code,
redirect_uri = redirect_uri,
client_id = _client_id,
client_secret = _client_secret,
grant_type = "authorization_code",
}
requesting = Requests.EXCHANGE_TOKEN
var err = request(request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(exchange_token_body))
if err != OK:
is_busy = false
Firebase._printerr("Error exchanging tokens: %s" % err)
# Open a web page in browser redirecting to Google oAuth2 page for the current project
# Once given user's authorization, a token will be generated.
# NOTE** with this method, the authorization process will be copy-pasted
func get_google_auth_manual(provider: AuthProvider = _local_provider) -> void:
provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
get_auth_with_redirect(provider)
# A timer used to listen through TCP checked the redirect uri of the request
func _tcp_stream_timer() -> void:
var peer : StreamPeer = tcp_server.take_connection()
if peer != null:
var raw_result : String = peer.get_utf8_string(441)
if raw_result != "" and raw_result.begins_with("GET"):
tcp_timer.stop()
remove_child(tcp_timer)
has_child = false
var token : String = ""
for value in raw_result.split(" ")[1].lstrip("/?").split("&"):
var splitted: PackedStringArray = value.split("=")
if _local_provider.params.response_type in splitted[0]:
token = splitted[1]
break
if token == "":
login_failed.emit()
peer.disconnect_from_host()
tcp_server.stop()
return
var data : PackedByteArray = '<p style="text-align:center">&#128293; You can close this window now. &#128293;</p>'.to_ascii_buffer()
peer.put_data(("HTTP/1.1 200 OK\n").to_ascii_buffer())
peer.put_data(("Server: Godot Firebase SDK\n").to_ascii_buffer())
peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii_buffer())
peer.put_data("Connection: close\n".to_ascii_buffer())
peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii_buffer())
peer.put_data(data)
login_with_oauth(token, _local_provider)
await self.login_succeeded
peer.disconnect_from_host()
tcp_server.stop()
# Function used to logout of the system, this will also remove_at the local encrypted auth file if there is one
func logout() -> void:
auth = {}
remove_auth()
logged_out.emit()
# Checks to see if we need a hard login
func needs_login() -> bool:
var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey)
var err = encrypted_file == null
return err
# Function is called when requesting a manual token refresh
func manual_token_refresh(auth_data):
auth = auth_data
var refresh_token = null
auth = get_clean_keys(auth)
if auth.has("refreshtoken"):
refresh_token = auth.refreshtoken
elif auth.has("refresh_token"):
refresh_token = auth.refresh_token
_needs_refresh = true
_refresh_request_body.refresh_token = refresh_token
var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body))
if err != OK:
is_busy = false
Firebase._printerr("Error manually refreshing token: %s" % err)
# This function is called whenever there is an authentication request to Firebase
# On an error, this function with emit the signal 'login_failed' and print the error to the console
func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
var json = Utilities.get_json_data(body.get_string_from_utf8())
is_busy = false
var res
if response_code == 0:
# Mocked error results to trigger the correct signal.
# Can occur if there is no internet connection, or the service is down,
# in which case there is no json_body (and thus parsing would fail).
res = {"error": {
"code": "Connection error",
"message": "Error connecting to auth service"}}
else:
if json == null:
Firebase._printerr("Error while parsing auth body json")
auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json")
return
res = json
if response_code == HTTPClient.RESPONSE_OK:
if not res.has("kind"):
auth = get_clean_keys(res)
match requesting:
Requests.EXCHANGE_TOKEN:
token_exchanged.emit(true)
begin_refresh_countdown()
# Refresh token countdown
auth_request.emit(1, auth)
if _needs_refresh:
_needs_refresh = false
if not is_oauth_login: login_succeeded.emit(auth)
else:
match res.kind:
RESPONSE_SIGNUP:
auth = get_clean_keys(res)
signup_succeeded.emit(auth)
begin_refresh_countdown()
RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN:
auth = get_clean_keys(res)
login_succeeded.emit(auth)
begin_refresh_countdown()
RESPONSE_USERDATA:
var userdata = FirebaseUserData.new(res.users[0])
userdata_received.emit(userdata)
auth_request.emit(1, auth)
else:
# error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD
if requesting == Requests.EXCHANGE_TOKEN:
token_exchanged.emit(false)
login_failed.emit(res.error, res.error_description)
auth_request.emit(res.error, res.error_description)
else:
var sig = signup_failed if auth_request_type == Auth_Type.SIGNUP_EP else login_failed
sig.emit(res.error.code, res.error.message)
auth_request.emit(res.error.code, res.error.message)
requesting = Requests.NONE
auth_request_type = Auth_Type.NONE
is_oauth_login = false
# Function used to save the auth data provided by Firebase into an encrypted file
# Note this does not work in HTML5 or UWP
func save_auth(auth : Dictionary) -> bool:
var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey)
var err = encrypted_file == null
if err:
Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error()))
else:
encrypted_file.store_line(JSON.stringify(auth))
return not err
# Function used to load the auth data file that has been stored locally
# Note this does not work in HTML5 or UWP
func load_auth() -> bool:
var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey)
var err = encrypted_file == null
if err:
Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(FileAccess.get_open_error()))
auth_request.emit(err, "Error Opening Firebase Auth File.")
else:
var json = JSON.new()
var json_parse_result = json.parse(encrypted_file.get_line())
if json_parse_result == OK:
var encrypted_file_data = json.data
manual_token_refresh(encrypted_file_data)
return not err
# Function used to remove_at the local encrypted auth file
func remove_auth() -> void:
if (FileAccess.file_exists("user://user.auth")):
DirAccess.remove_absolute("user://user.auth")
else:
Firebase._printerr("No encrypted auth file exists")
# Function to check if there is an encrypted auth data file
# If there is, the game will load it and refresh the token
func check_auth_file() -> bool:
if (FileAccess.file_exists("user://user.auth")):
# Will ensure "auth_request" emitted
return load_auth()
else:
Firebase._printerr("Encrypted Firebase Auth file does not exist")
auth_request.emit(ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist")
return false
# Function used to change the email account for the currently logged in user
func change_user_email(email : String) -> void:
if _is_ready():
is_busy = true
_change_email_body.email = email
_change_email_body.idToken = auth.idtoken
var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_email_body))
if err != OK:
is_busy = false
Firebase._printerr("Error changing user email: %s" % err)
# Function used to change the password for the currently logged in user
func change_user_password(password : String) -> void:
if _is_ready():
is_busy = true
_change_password_body.password = password
_change_password_body.idToken = auth.idtoken
var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_password_body))
if err != OK:
is_busy = false
Firebase._printerr("Error changing user password: %s" % err)
# User Profile handlers
func update_account(idToken : String, displayName : String, photoUrl : String, deleteAttribute : PackedStringArray, returnSecureToken : bool) -> void:
if _is_ready():
is_busy = true
_update_profile_body.idToken = idToken
_update_profile_body.displayName = displayName
_update_profile_body.photoUrl = photoUrl
_update_profile_body.deleteAttribute = deleteAttribute
_update_profile_body.returnSecureToken = returnSecureToken
var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_update_profile_body))
if err != OK:
is_busy = false
Firebase._printerr("Error updating account: %s" % err)
# Link account with Email and Password
func link_account(email : String, password : String) -> void:
if _is_ready():
is_busy = true
link_account_body.idToken = auth.idtoken
link_account_body.email = email
link_account_body.password = password
var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(link_account_body))
if err != OK:
is_busy = false
Firebase._printerr("Error updating account: %s" % err)
# Function to send a account verification email
func send_account_verification_email() -> void:
if _is_ready():
is_busy = true
_account_verification_body.idToken = auth.idtoken
var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_account_verification_body))
if err != OK:
is_busy = false
Firebase._printerr("Error sending account verification email: %s" % err)
# Function used to reset the password for a user who has forgotten in.
# This will send the users account an email with a password reset link
func send_password_reset_email(email : String) -> void:
if _is_ready():
is_busy = true
_password_reset_body.email = email
var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_password_reset_body))
if err != OK:
is_busy = false
Firebase._printerr("Error sending password reset email: %s" % err)
# Function called to get all
func get_user_data() -> void:
if _is_ready():
is_busy = true
if not is_logged_in():
print_debug("Not logged in")
is_busy = false
return
var err = request(_base_url + _userdata_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken}))
if err != OK:
is_busy = false
Firebase._printerr("Error getting user data: %s" % err)
# Function used to delete the account of the currently authenticated user
func delete_user_account() -> void:
if _is_ready():
is_busy = true
var err = request(_base_url + _delete_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken}))
if err != OK:
is_busy = false
Firebase._printerr("Error deleting user: %s" % err)
else:
remove_auth()
# Function is called when a new token is issued to a user. The function will yield until the token has expired, and then request a new one.
func begin_refresh_countdown() -> void:
var refresh_token = null
var expires_in = 1000
auth = get_clean_keys(auth)
if auth.has("refreshtoken"):
refresh_token = auth.refreshtoken
expires_in = auth.expiresin
elif auth.has("refresh_token"):
refresh_token = auth.refresh_token
expires_in = auth.expires_in
if auth.has("userid"):
auth["localid"] = auth.userid
_needs_refresh = true
token_refresh_succeeded.emit(auth)
await get_tree().create_timer(float(expires_in)).timeout
_refresh_request_body.refresh_token = refresh_token
var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body))
if err != OK:
is_busy = false
Firebase._printerr("Error refreshing via countdown: %s" % err)
func get_token_from_url(provider: AuthProvider):
var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token"
if OS.has_feature('web'):
var token = JavaScriptBridge.eval("""
var url_string = window.location.href.replaceAll('?#', '?');
var url = new URL(url_string);
url.searchParams.get('"""+token_type+"""');
""")
JavaScriptBridge.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""")
return token
return null
func set_redirect_uri(redirect_uri : String) -> void:
self._local_uri = redirect_uri
func set_local_provider(provider : AuthProvider) -> void:
self._local_provider = provider
# This function is used to make all keys lowercase
# This is only used to cut down checked processing errors from Firebase
# This is due to Google have inconsistencies in the API that we are trying to fix
func get_clean_keys(auth_result : Dictionary) -> Dictionary:
var cleaned = {}
for key in auth_result.keys():
cleaned[key.replace("_", "").to_lower()] = auth_result[key]
return cleaned
# --------------------
# PROVIDERS
# --------------------
func get_GoogleProvider() -> GoogleProvider:
return GoogleProvider.new(_config.clientId, _config.clientSecret)
func get_FacebookProvider() -> FacebookProvider:
return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret)
func get_GitHubProvider() -> GitHubProvider:
return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret)
func get_TwitterProvider() -> TwitterProvider:
return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret)

View File

@@ -0,0 +1 @@
uid://bvlvihyfqktvr

View File

@@ -0,0 +1,32 @@
@tool
class_name AuthProvider
extends RefCounted
var redirect_uri: String = ""
var access_token_uri: String = ""
var provider_id: String = ""
var params: Dictionary = {
client_id = "",
scope = "",
response_type = "",
state = "",
redirect_type = "redirect_uri",
}
var client_secret: String = ""
var should_exchange: bool = false
func set_client_id(client_id: String) -> void:
self.params.client_id = client_id
func set_client_secret(client_secret: String) -> void:
self.client_secret = client_secret
func get_client_id() -> String:
return self.params.client_id
func get_client_secret() -> String:
return self.client_secret
func get_oauth_params() -> String:
return ""

View File

@@ -0,0 +1 @@
uid://i5o82uhvlsyj

View File

@@ -0,0 +1,21 @@
class_name FacebookProvider
extends AuthProvider
func _init(client_id: String,client_secret: String):
randomize()
set_client_id(client_id)
set_client_secret(client_secret)
self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?"
self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token"
self.provider_id = "facebook.com"
self.params.scope = "public_profile"
self.params.state = str(randf_range(0, 1))
if Utilities.is_web():
self.should_exchange = false
self.params.response_type = "token"
else:
self.should_exchange = true
self.params.response_type = "code"

View File

@@ -0,0 +1 @@
uid://s5c12vma7ufy

View File

@@ -0,0 +1,14 @@
class_name GitHubProvider
extends AuthProvider
func _init(client_id: String,client_secret: String):
randomize()
set_client_id(client_id)
set_client_secret(client_secret)
self.should_exchange = true
self.redirect_uri = "https://github.com/login/oauth/authorize?"
self.access_token_uri = "https://github.com/login/oauth/access_token"
self.provider_id = "github.com"
self.params.scope = "user:read"
self.params.state = str(randf_range(0, 1))
self.params.response_type = "code"

View File

@@ -0,0 +1 @@
uid://gactahg7liyw

View File

@@ -0,0 +1,13 @@
class_name GoogleProvider
extends AuthProvider
func _init(client_id: String,client_secret: String):
set_client_id(client_id)
set_client_secret(client_secret)
self.should_exchange = true
self.redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth?"
self.access_token_uri = "https://oauth2.googleapis.com/token"
self.provider_id = "google.com"
self.params.response_type = "code"
self.params.scope = "email openid profile"
self.params.response_type = "code"

View File

@@ -0,0 +1 @@
uid://dkl7y6dl8fg8s

View File

@@ -0,0 +1,39 @@
class_name TwitterProvider
extends AuthProvider
var request_token_endpoint: String = "https://api.twitter.com/oauth/access_token?oauth_callback="
var oauth_header: Dictionary = {
oauth_callback="",
oauth_consumer_key="",
oauth_nonce="",
oauth_signature="",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="",
oauth_version="1.0"
}
func _init(client_id: String,client_secret: String):
randomize()
set_client_id(client_id)
set_client_secret(client_secret)
self.oauth_header.oauth_consumer_key = client_id
self.oauth_header.oauth_nonce = Time.get_ticks_usec()
self.oauth_header.oauth_timestamp = Time.get_ticks_msec()
self.should_exchange = true
self.redirect_uri = "https://twitter.com/i/oauth2/authorize?"
self.access_token_uri = "https://api.twitter.com/2/oauth2/token"
self.provider_id = "twitter.com"
self.params.redirect_type = "redirect_uri"
self.params.response_type = "code"
self.params.scope = "users.read"
self.params.state = str(randf_range(0, 1))
func get_oauth_params() -> String:
var params: PackedStringArray = []
for key in self.oauth.keys():
params.append(key+"="+self.oauth.get(key))
return "&".join(params)

View File

@@ -0,0 +1 @@
uid://bwvku5unmemyb

View File

@@ -0,0 +1,44 @@
## @meta-authors TODO
## @meta-version 2.3
## Authentication user data.
## Documentation TODO.
@tool
class_name FirebaseUserData
extends RefCounted
var local_id : String = "" # The uid of the current user.
var email : String = ""
var email_verified := false # Whether or not the account's email has been verified.
var password_updated_at : float = 0 # The timestamp, in milliseconds, that the account password was last changed.
var last_login_at : float = 0 # The timestamp, in milliseconds, that the account last logged in at.
var created_at : float = 0 # The timestamp, in milliseconds, that the account was created at.
var provider_user_info : Array = []
var provider_id : String = ""
var display_name : String = ""
var photo_url : String = ""
func _init(p_userdata : Dictionary):
local_id = p_userdata.get("localId", "")
email = p_userdata.get("email", "")
email_verified = p_userdata.get("emailVerified", false)
last_login_at = float(p_userdata.get("lastLoginAt", 0))
created_at = float(p_userdata.get("createdAt", 0))
password_updated_at = float(p_userdata.get("passwordUpdatedAt", 0))
display_name = p_userdata.get("displayName", "")
provider_user_info = p_userdata.get("providerUserInfo", [])
if not provider_user_info.is_empty():
provider_id = provider_user_info[0].get("providerId", "")
photo_url = provider_user_info[0].get("photoUrl", "")
display_name = provider_user_info[0].get("displayName", "")
func as_text() -> String:
return _to_string()
func _to_string() -> String:
var txt = "local_id : %s\n" % local_id
txt += "email : %s\n" % email
txt += "last_login_at : %d\n" % last_login_at
txt += "provider_id : %s\n" % provider_id
txt += "display name : %s\n" % display_name
return txt

View File

@@ -0,0 +1 @@
uid://d1njxyqnur3a1

View File

@@ -0,0 +1,51 @@
## @meta-authors TODO
## @meta-version 2.2
## The Realtime Database API for Firebase.
## Documentation TODO.
@tool
class_name FirebaseDatabase
extends Node
var _base_url : String = ""
var _config : Dictionary = {}
var _auth : Dictionary = {}
func _set_config(config_json : Dictionary) -> void:
_config = config_json
_check_emulating()
func _check_emulating() -> void :
## Check emulating
if not Firebase.emulating:
_base_url = _config.databaseURL
else:
var port : String = _config.emulators.ports.realtimeDatabase
if port == "":
Firebase._printerr("You are in 'emulated' mode, but the port for Realtime Database has not been configured.")
else:
_base_url = "http://localhost"
func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void:
_auth = auth_result
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
_auth = auth_result
func _on_FirebaseAuth_logout() -> void:
_auth = {}
func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference:
var firebase_reference = load("res://addons/godot-firebase/database/firebase_database_reference.tscn").instantiate()
firebase_reference.set_db_path(path, filter)
firebase_reference.set_auth_and_config(_auth, _config)
add_child(firebase_reference)
return firebase_reference
func get_once_database_reference(path : String, filter : Dictionary = {}) -> FirebaseOnceDatabaseReference:
var firebase_reference = load("res://addons/godot-firebase/database/firebase_once_database_reference.tscn").instantiate()
firebase_reference.set_db_path(path, filter)
firebase_reference.set_auth_and_config(_auth, _config)
add_child(firebase_reference)
return firebase_reference

View File

@@ -0,0 +1 @@
uid://ba2spgo4jo2qw

View File

@@ -0,0 +1,109 @@
## @meta-authors TODO
## @meta-version 2.2
## Data structure that holds the currently-known data at a given path (a.k.a. reference) in a Firebase Realtime Database.
## Can process both puts and patches into the data based checked realtime events received from the service.
@tool
class_name FirebaseDatabaseStore
extends Node
const _DELIMITER : String = "/"
const _ROOT : String = "_root"
## @default false
## Whether the store is in debug mode.
var debug : bool = false
var _data : Dictionary = { }
## @args path, payload
## Puts a new payload into this data store at the given path. Any existing values in this data store
## at the specified path will be completely erased.
func put(path : String, payload) -> void:
_update_data(path, payload, false)
## @args path, payload
## Patches an update payload into this data store at the specified path.
## NOTE: When patching in updates to arrays, payload should contain the entire new array! Updating single elements/indexes of an array is not supported. Sometimes when manually mutating array data directly from the Firebase Realtime Database console, single-element patches will be sent out which can cause issues here.
func patch(path : String, payload) -> void:
_update_data(path, payload, true)
## @args path, payload
## Deletes data at the reference point provided
## NOTE: This will delete without warning, so make sure the reference is pointed to the level you want and not the root or you will lose everything
func delete(path : String, payload) -> void:
_update_data(path, payload, true)
## Returns a deep copy of this data store's payload.
func get_data() -> Dictionary:
return _data[_ROOT].duplicate(true)
#
# Updates this data store by either putting or patching the provided payload into it at the given
# path. The provided payload can technically be any value.
#
func _update_data(path: String, payload, patch: bool) -> void:
if debug:
print("Updating data store (patch = %s) (%s = %s)..." % [patch, path, payload])
#
# Remove any leading separators.
#
path = path.lstrip(_DELIMITER)
#
# Traverse the path.
#
var dict = _data
var keys = PackedStringArray([_ROOT])
keys.append_array(path.split(_DELIMITER, false))
var final_key_idx = (keys.size() - 1)
var final_key = (keys[final_key_idx])
keys.remove_at(final_key_idx)
for key in keys:
if !dict.has(key):
dict[key] = { }
dict = dict[key]
#
# Handle non-patch (a.k.a. put) mode and then update the destination value.
#
var new_type = typeof(payload)
if !patch:
dict.erase(final_key)
if new_type == TYPE_NIL:
dict.erase(final_key)
elif new_type == TYPE_DICTIONARY:
if !dict.has(final_key):
dict[final_key] = { }
_update_dictionary(dict[final_key], payload)
else:
dict[final_key] = payload
if debug:
print("...Data store updated (%s)." % _data)
#
# Helper method to "blit" changes in an update dictionary payload onto an original dictionary.
# Parameters are directly changed via reference.
#
func _update_dictionary(original_dict: Dictionary, update_payload: Dictionary) -> void:
for key in update_payload.keys():
var val_type = typeof(update_payload[key])
if val_type == TYPE_NIL:
original_dict.erase(key)
elif val_type == TYPE_DICTIONARY:
if !original_dict.has(key):
original_dict[key] = { }
_update_dictionary(original_dict[key], update_payload[key])
else:
original_dict[key] = update_payload[key]

View File

@@ -0,0 +1 @@
uid://bo3i7q8s2bfmq

View File

@@ -0,0 +1,17 @@
[gd_scene load_steps=5 format=3 uid="uid://btltp52tywbe4"]
[ext_resource type="Script" path="res://addons/godot-firebase/database/reference.gd" id="1_l3oy5"]
[ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_0qpk7"]
[ext_resource type="Script" path="res://addons/http-sse-client/HTTPSSEClient.gd" id="2_4l0io"]
[ext_resource type="Script" path="res://addons/godot-firebase/database/database_store.gd" id="3_c3r2w"]
[node name="FirebaseDatabaseReference" type="Node"]
script = ExtResource("1_l3oy5")
[node name="Pusher" parent="." instance=ExtResource("2_0qpk7")]
[node name="Listener" type="Node" parent="."]
script = ExtResource("2_4l0io")
[node name="DataStore" type="Node" parent="."]
script = ExtResource("3_c3r2w")

View File

@@ -0,0 +1,16 @@
[gd_scene load_steps=3 format=3 uid="uid://d1u1bxp2fd60e"]
[ext_resource type="Script" path="res://addons/godot-firebase/database/once_reference.gd" id="1_hq5s2"]
[ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_t2f32"]
[node name="FirebaseOnceDatabaseReference" type="Node"]
script = ExtResource("1_hq5s2")
[node name="Pusher" parent="." instance=ExtResource("2_t2f32")]
accept_gzip = false
[node name="Oncer" parent="." instance=ExtResource("2_t2f32")]
accept_gzip = false
[connection signal="queue_request_completed" from="Pusher" to="." method="on_push_request_complete"]
[connection signal="queue_request_completed" from="Oncer" to="." method="on_get_request_complete"]

View File

@@ -0,0 +1,124 @@
class_name FirebaseOnceDatabaseReference
extends Node
## @meta-authors BackAt50Ft
## @meta-version 1.0
## A once off reference to a location in the Realtime Database.
## Documentation TODO.
signal once_successful(dataSnapshot)
signal once_failed()
signal push_successful()
signal push_failed()
const ORDER_BY : String = "orderBy"
const LIMIT_TO_FIRST : String = "limitToFirst"
const LIMIT_TO_LAST : String = "limitToLast"
const START_AT : String = "startAt"
const END_AT : String = "endAt"
const EQUAL_TO : String = "equalTo"
@onready var _oncer = $Oncer
@onready var _pusher = $Pusher
var _auth : Dictionary
var _config : Dictionary
var _filter_query : Dictionary
var _db_path : String
const _separator : String = "/"
const _json_list_tag : String = ".json"
const _query_tag : String = "?"
const _auth_tag : String = "auth="
const _auth_variable_begin : String = "["
const _auth_variable_end : String = "]"
const _filter_tag : String = "&"
const _escaped_quote : String = '"'
const _equal_tag : String = "="
const _key_filter_tag : String = "$key"
var _headers : PackedStringArray = []
func set_db_path(path : String, filter_query_dict : Dictionary) -> void:
_db_path = path
_filter_query = filter_query_dict
func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void:
_auth = auth_ref
_config = config_ref
#
# Gets a data snapshot once at the position passed in
#
func once(reference : String) -> void:
var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path()
_oncer.request(ref_pos, _headers, HTTPClient.METHOD_GET, "")
func _get_remaining_path(is_push : bool = true) -> String:
var remaining_path = ""
if _filter_query_empty() or is_push:
remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken
else:
remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken
if Firebase.emulating:
remaining_path += "&ns="+_config.projectId+"-default-rtdb"
return remaining_path
func _get_list_url(with_port:bool = true) -> String:
var url = Firebase.Database._base_url.trim_suffix(_separator)
if with_port and Firebase.emulating:
url += ":" + _config.emulators.ports.realtimeDatabase
return url + _separator
func _get_filter():
if _filter_query_empty():
return ""
var filter = ""
if _filter_query.has(ORDER_BY):
filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote
_filter_query.erase(ORDER_BY)
else:
filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all...
for key in _filter_query.keys():
filter += _filter_tag + key + _equal_tag + _filter_query[key]
return filter
func _filter_query_empty() -> bool:
return _filter_query == null or _filter_query.is_empty()
func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
if response_code == HTTPClient.RESPONSE_OK:
var bod = Utilities.get_json_data(body)
once_successful.emit(bod)
else:
once_failed.emit()
func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
if response_code == HTTPClient.RESPONSE_OK:
push_successful.emit()
else:
push_failed.emit()
func push(data : Dictionary) -> void:
var to_push = JSON.stringify(data)
_pusher.request(_get_list_url() + _db_path + _get_remaining_path(true), _headers, HTTPClient.METHOD_POST, to_push)
func update(path : String, data : Dictionary) -> void:
path = path.strip_edges(true, true)
if path == _separator:
path = ""
var to_update = JSON.stringify(data)
var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path())
_pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update)

View File

@@ -0,0 +1 @@
uid://d0b2x1kc1w1w3

View File

@@ -0,0 +1,176 @@
## @meta-authors BackAt50Ft
## @meta-version 2.4
## A reference to a location in the Realtime Database.
## Documentation TODO.
@tool
class_name FirebaseDatabaseReference
extends Node
signal new_data_update(data)
signal patch_data_update(data)
signal delete_data_update(data)
signal once_successful(dataSnapshot)
signal once_failed()
signal push_successful()
signal push_failed()
const ORDER_BY : String = "orderBy"
const LIMIT_TO_FIRST : String = "limitToFirst"
const LIMIT_TO_LAST : String = "limitToLast"
const START_AT : String = "startAt"
const END_AT : String = "endAt"
const EQUAL_TO : String = "equalTo"
@onready var _pusher := $Pusher
@onready var _listener := $Listener
@onready var _store := $DataStore
var _auth : Dictionary
var _config : Dictionary
var _filter_query : Dictionary
var _db_path : String
var _cached_filter : String
var _can_connect_to_host : bool = false
const _put_tag : String = "put"
const _patch_tag : String = "patch"
const _delete_tag : String = "delete"
const _separator : String = "/"
const _json_list_tag : String = ".json"
const _query_tag : String = "?"
const _auth_tag : String = "auth="
const _accept_header : String = "accept: text/event-stream"
const _auth_variable_begin : String = "["
const _auth_variable_end : String = "]"
const _filter_tag : String = "&"
const _escaped_quote : String = '"'
const _equal_tag : String = "="
const _key_filter_tag : String = "$key"
var _headers : PackedStringArray = []
func _ready() -> void:
#region Set Listener info
$Listener.new_sse_event.connect(on_new_sse_event)
var base_url = _get_list_url(false).trim_suffix(_separator)
var extended_url = _separator + _db_path + _get_remaining_path(false)
var port = -1
if Firebase.emulating:
port = int(_config.emulators.ports.realtimeDatabase)
$Listener.connect_to_host(base_url, extended_url, port)
#endregion Set Listener info
#region Set Pusher info
$Pusher.queue_request_completed.connect(on_push_request_complete)
#endregion Set Pusher info
func set_db_path(path : String, filter_query_dict : Dictionary) -> void:
_db_path = path
_filter_query = filter_query_dict
func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void:
_auth = auth_ref
_config = config_ref
func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) -> void:
if data:
var command = event
if command and command != "keep-alive":
_route_data(command, data.path, data.data)
if command == _put_tag:
if data.path == _separator and data.data and data.data.keys().size() > 0:
for key in data.data.keys():
new_data_update.emit(FirebaseResource.new(_separator + key, data.data[key]))
elif data.path != _separator:
new_data_update.emit(FirebaseResource.new(data.path, data.data))
elif command == _patch_tag:
patch_data_update.emit(FirebaseResource.new(data.path, data.data))
elif command == _delete_tag:
delete_data_update.emit(FirebaseResource.new(data.path, data.data))
func update(path : String, data : Dictionary) -> void:
path = path.strip_edges(true, true)
if path == _separator:
path = ""
var to_update = JSON.stringify(data)
var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path())
_pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update)
func push(data : Dictionary) -> void:
var to_push = JSON.stringify(data)
_pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push)
func delete(reference : String) -> void:
_pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "")
#
# Returns a deep copy of the current local copy of the data stored at this reference in the Firebase
# Realtime Database.
#
func get_data() -> Dictionary:
if _store == null:
return { }
return _store.get_data()
func _get_remaining_path(is_push : bool = true) -> String:
var remaining_path = ""
if _filter_query_empty() or is_push:
remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken
else:
remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken
if Firebase.emulating:
remaining_path += "&ns="+_config.projectId+"-default-rtdb"
return remaining_path
func _get_list_url(with_port:bool = true) -> String:
var url = Firebase.Database._base_url.trim_suffix(_separator)
if with_port and Firebase.emulating:
url += ":" + _config.emulators.ports.realtimeDatabase
return url + _separator
func _get_filter():
if _filter_query_empty():
return ""
# At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules.
if _cached_filter != "":
_cached_filter = ""
if _filter_query.has(ORDER_BY):
_cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote
_filter_query.erase(ORDER_BY)
else:
_cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all...
for key in _filter_query.keys():
_cached_filter += _filter_tag + key + _equal_tag + _filter_query[key]
return _cached_filter
func _filter_query_empty() -> bool:
return _filter_query == null or _filter_query.is_empty()
#
# Appropriately updates the current local copy of the data stored at this reference in the Firebase
# Realtime Database.
#
func _route_data(command : String, path : String, data) -> void:
if command == _put_tag:
_store.put(path, data)
elif command == _patch_tag:
_store.patch(path, data)
elif command == _delete_tag:
_store.delete(path, data)
func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
if response_code == HTTPClient.RESPONSE_OK:
push_successful.emit()
else:
push_failed.emit()

View File

@@ -0,0 +1 @@
uid://ofyy1lc3qlfn

View File

@@ -0,0 +1,16 @@
## @meta-authors SIsilicon, fenix-hub
## @meta-version 2.2
## A generic resource used by Firebase Database.
@tool
class_name FirebaseResource
extends Resource
var key : String
var data
func _init(key : String,data):
self.key = key.lstrip("/")
self.data = data
func _to_string():
return "{ key:{key}, data:{data} }".format({key = key, data = data})

View File

@@ -0,0 +1 @@
uid://cbqame2gc2atr

View File

@@ -0,0 +1,109 @@
## @meta-authors TODO
## @meta-authors TODO
## @meta-version 1.1
## The dynamic links API for Firebase
## Documentation TODO.
@tool
class_name FirebaseDynamicLinks
extends Node
signal dynamic_link_generated(link_result)
signal generate_dynamic_link_error(error)
const _AUTHORIZATION_HEADER : String = "Authorization: Bearer "
const _API_VERSION : String = "v1"
var request : int = -1
var _base_url : String = ""
var _config : Dictionary = {}
var _auth : Dictionary
var _request_list_node : HTTPRequest
var _headers : PackedStringArray = []
enum Requests {
NONE = -1,
GENERATE
}
func _set_config(config_json : Dictionary) -> void:
_config = config_json
_request_list_node = HTTPRequest.new()
Utilities.fix_http_request(_request_list_node)
_request_list_node.request_completed.connect(_on_request_completed)
add_child(_request_list_node)
_check_emulating()
func _check_emulating() -> void :
## Check emulating
if not Firebase.emulating:
_base_url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=%s"
_base_url %= _config.apiKey
else:
var port : String = _config.emulators.ports.dynamicLinks
if port == "":
Firebase._printerr("You are in 'emulated' mode, but the port for Dynamic Links has not been configured.")
else:
_base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port })
var _link_request_body : Dictionary = {
"dynamicLinkInfo": {
"domainUriPrefix": "",
"link": "",
"androidInfo": {
"androidPackageName": ""
},
"iosInfo": {
"iosBundleId": ""
}
},
"suffix": {
"option": ""
}
}
## @args log_link, APN, IBI, is_unguessable
## This function is used to generate a dynamic link using the Firebase REST API
## It will return a JSON with the shortened link
func generate_dynamic_link(long_link : String, APN : String, IBI : String, is_unguessable : bool) -> void:
if not _config.domainUriPrefix or _config.domainUriPrefix == "":
generate_dynamic_link_error.emit("Error: Missing domainUriPrefix in config file. Parameter is required.")
Firebase._printerr("Error: Missing domainUriPrefix in config file. Parameter is required.")
return
request = Requests.GENERATE
_link_request_body.dynamicLinkInfo.domainUriPrefix = _config.domainUriPrefix
_link_request_body.dynamicLinkInfo.link = long_link
_link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN
_link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI
if is_unguessable:
_link_request_body.suffix.option = "UNGUESSABLE"
else:
_link_request_body.suffix.option = "SHORT"
_request_list_node.request(_base_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_link_request_body))
func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
var json = JSON.new()
var json_parse_result = json.parse(body.get_string_from_utf8())
if json_parse_result == OK:
var result_body = json.data.result # Check this
dynamic_link_generated.emit(result_body.shortLink)
else:
generate_dynamic_link_error.emit(json.get_error_message())
# This used to return immediately when above, but it should still clear the request, so removing it
request = Requests.NONE
func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void:
_auth = auth_result
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
_auth = auth_result
func _on_FirebaseAuth_logout() -> void:
_auth = {}

View File

@@ -0,0 +1 @@
uid://dgbj7nixmfyrs

View File

@@ -0,0 +1,144 @@
## @meta-authors Kyle Szklenski
## @meta-version 2.6
## The Firebase Godot API.
## This singleton gives you access to your Firebase project and its capabilities. Using this requires you to fill out some Firebase configuration settings. It currently comes with four modules.
## - [code]Auth[/code]: Manages user authentication (logging and out, etc...)
## - [code]Database[/code]: A NonSQL realtime database for managing data in JSON structures.
## - [code]Firestore[/code]: Similar to Database, but stores data in collections and documents, among other things.
## - [code]Storage[/code]: Gives access to Cloud Storage; perfect for storing files like images and other assets.
## - [code]RemoteConfig[/code]: Gives access to Remote Config functionality; allows you to download your app's configuration from Firebase, do A/B testing, and more.
##
## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki
@tool
extends Node
const _ENVIRONMENT_VARIABLES : String = "firebase/environment_variables"
const _EMULATORS_PORTS : String = "firebase/emulators/ports"
const _AUTH_PROVIDERS : String = "firebase/auth_providers"
## @type FirebaseAuth
## The Firebase Authentication API.
@onready var Auth := $Auth
## @type FirebaseFirestore
## The Firebase Firestore API.
@onready var Firestore := $Firestore
## @type FirebaseDatabase
## The Firebase Realtime Database API.
@onready var Database := $Database
## @type FirebaseStorage
## The Firebase Storage API.
@onready var Storage := $Storage
## @type FirebaseDynamicLinks
## The Firebase Dynamic Links API.
@onready var DynamicLinks := $DynamicLinks
## @type FirebaseFunctions
## The Firebase Cloud Functions API
@onready var Functions := $Functions
## @type FirebaseRemoteConfig
## The Firebase Remote Config API
@onready var RemoteConfigAPI := $RemoteConfig
@export var emulating : bool = false
# Configuration used by all files in this project
# These values can be found in your Firebase Project
# See the README checked Github for how to access
var _config : Dictionary = {
"apiKey": "",
"authDomain": "",
"databaseURL": "",
"projectId": "",
"storageBucket": "",
"messagingSenderId": "",
"appId": "",
"measurementId": "",
"clientId": "",
"clientSecret" : "",
"domainUriPrefix" : "",
"functionsGeoZone" : "",
"cacheLocation":"",
"emulators": {
"ports" : {
"authentication" : "",
"firestore" : "",
"realtimeDatabase" : "",
"functions" : "",
"storage" : "",
"dynamicLinks" : ""
}
},
"workarounds":{
"database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329
},
"auth_providers": {
"facebook_id":"",
"facebook_secret":"",
"github_id":"",
"github_secret":"",
"twitter_id":"",
"twitter_secret":""
}
}
func _ready() -> void:
_load_config()
func set_emulated(emulating : bool = true) -> void:
self.emulating = emulating
_check_emulating()
func _check_emulating() -> void:
if emulating:
print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.")
for module in get_children():
if module.has_method("_check_emulating"):
module._check_emulating()
func _load_config() -> void:
if not (_config.apiKey != "" and _config.authDomain != ""):
var env = ConfigFile.new()
var err = env.load("res://addons/godot-firebase/.env")
if err == OK:
for key in _config.keys():
var config_value = _config[key]
if key == "emulators" and config_value.has("ports"):
for port in config_value["ports"].keys():
config_value["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "")
if key == "auth_providers":
for provider in config_value.keys():
config_value[provider] = env.get_value(_AUTH_PROVIDERS, provider, "")
else:
var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "")
if value == "":
_print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key)
else:
_config[key] = value
else:
_printerr("Unable to read .env file at path 'res://addons/godot-firebase/.env'")
_setup_modules()
func _setup_modules() -> void:
for module in get_children():
module._set_config(_config)
if not module.has_method("_on_FirebaseAuth_login_succeeded"):
continue
Auth.login_succeeded.connect(module._on_FirebaseAuth_login_succeeded)
Auth.signup_succeeded.connect(module._on_FirebaseAuth_login_succeeded)
Auth.token_refresh_succeeded.connect(module._on_FirebaseAuth_token_refresh_succeeded)
Auth.logged_out.connect(module._on_FirebaseAuth_logout)
# -------------
func _printerr(error : String) -> void:
printerr("[Firebase Error] >> " + error)
func _print(msg : String) -> void:
print("[Firebase] >> " + str(msg))

View File

@@ -0,0 +1 @@
uid://c0vsvs7ol6n0x

View File

@@ -0,0 +1,36 @@
[gd_scene load_steps=9 format=3 uid="uid://cvb26atjckwlq"]
[ext_resource type="Script" path="res://addons/godot-firebase/database/database.gd" id="1"]
[ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore.gd" id="2"]
[ext_resource type="Script" path="res://addons/godot-firebase/firebase/firebase.gd" id="3"]
[ext_resource type="Script" path="res://addons/godot-firebase/auth/auth.gd" id="4"]
[ext_resource type="Script" path="res://addons/godot-firebase/storage/storage.gd" id="5"]
[ext_resource type="Script" path="res://addons/godot-firebase/dynamiclinks/dynamiclinks.gd" id="6"]
[ext_resource type="Script" path="res://addons/godot-firebase/functions/functions.gd" id="7"]
[ext_resource type="PackedScene" uid="uid://5xa6ulbllkjk" path="res://addons/godot-firebase/remote_config/firebase_remote_config.tscn" id="8_mvdf4"]
[node name="Firebase" type="Node"]
script = ExtResource("3")
[node name="Auth" type="HTTPRequest" parent="."]
max_redirects = 12
timeout = 10.0
script = ExtResource("4")
[node name="Firestore" type="Node" parent="."]
script = ExtResource("2")
[node name="Database" type="Node" parent="."]
script = ExtResource("1")
[node name="Storage" type="Node" parent="."]
script = ExtResource("5")
[node name="DynamicLinks" type="Node" parent="."]
script = ExtResource("6")
[node name="Functions" type="Node" parent="."]
script = ExtResource("7")
[node name="RemoteConfig" parent="." instance=ExtResource("8_mvdf4")]
accept_gzip = false

View File

@@ -0,0 +1,22 @@
extends FirestoreTransform
class_name FieldTransform
enum TransformType { SetToServerValue, Maximum, Minimum, Increment, AppendMissingElements, RemoveAllFromArray }
const transtype_string_map = {
TransformType.SetToServerValue : "setToServerValue",
TransformType.Increment : "increment",
TransformType.Maximum : "maximum",
TransformType.Minimum : "minimum",
TransformType.AppendMissingElements : "appendMissingElements",
TransformType.RemoveAllFromArray : "removeAllFromArray"
}
var document_exists : bool
var document_name : String
var field_path : String
var transform_type : TransformType
var value : Variant
func get_transform_type() -> String:
return transtype_string_map[transform_type]

View File

@@ -0,0 +1 @@
uid://dw7p8101sxern

View File

@@ -0,0 +1,35 @@
class_name FieldTransformArray
extends RefCounted
var transforms = []
var _extended_url
var _collection_name
const _separator = "/"
func set_config(config : Dictionary):
_extended_url = config.extended_url
_collection_name = config.collection_name
func push_back(transform : FieldTransform) -> void:
transforms.push_back(transform)
func serialize() -> Dictionary:
var body = {}
var writes_array = []
for transform in transforms:
writes_array.push_back({
"currentDocument": { "exists" : transform.document_exists },
"transform" : {
"document": _extended_url + _collection_name + _separator + transform.document_name,
"fieldTransforms": [
{
"fieldPath": transform.field_path,
transform.get_transform_type(): transform.value
}]
}
})
body = { "writes": writes_array }
return body

View File

@@ -0,0 +1 @@
uid://bopmc0mi55dvu

View File

@@ -0,0 +1,19 @@
class_name DecrementTransform
extends FieldTransform
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void:
document_name = doc_name
document_exists = doc_must_exist
field_path = path_to_field
transform_type = FieldTransform.TransformType.Increment
var value_type = typeof(by_this_much)
if value_type == TYPE_INT:
self.value = {
"integerValue": -by_this_much
}
elif value_type == TYPE_FLOAT:
self.value = {
"doubleValue": -by_this_much
}

View File

@@ -0,0 +1 @@
uid://bit26sxq4daw7

View File

@@ -0,0 +1,19 @@
class_name IncrementTransform
extends FieldTransform
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void:
document_name = doc_name
document_exists = doc_must_exist
field_path = path_to_field
transform_type = FieldTransform.TransformType.Increment
var value_type = typeof(by_this_much)
if value_type == TYPE_INT:
self.value = {
"integerValue": by_this_much
}
elif value_type == TYPE_FLOAT:
self.value = {
"doubleValue": by_this_much
}

View File

@@ -0,0 +1 @@
uid://c5gx3a3dsmwop

View File

@@ -0,0 +1,19 @@
class_name MaxTransform
extends FieldTransform
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void:
document_name = doc_name
document_exists = doc_must_exist
field_path = path_to_field
transform_type = FieldTransform.TransformType.Maximum
var value_type = typeof(value)
if value_type == TYPE_INT:
self.value = {
"integerValue": value
}
elif value_type == TYPE_FLOAT:
self.value = {
"doubleValue": value
}

View File

@@ -0,0 +1 @@
uid://xf5c8b0lrjpl

View File

@@ -0,0 +1,19 @@
class_name MinTransform
extends FieldTransform
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void:
document_name = doc_name
document_exists = doc_must_exist
field_path = path_to_field
transform_type = FieldTransform.TransformType.Minimum
var value_type = typeof(value)
if value_type == TYPE_INT:
self.value = {
"integerValue": value
}
elif value_type == TYPE_FLOAT:
self.value = {
"doubleValue": value
}

View File

@@ -0,0 +1 @@
uid://cei7mxm5uqrkc

View File

@@ -0,0 +1,10 @@
class_name ServerTimestampTransform
extends FieldTransform
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String) -> void:
document_name = doc_name
document_exists = doc_must_exist
field_path = path_to_field
transform_type = FieldTransform.TransformType.SetToServerValue
value = "REQUEST_TIME"

View File

@@ -0,0 +1 @@
uid://cqkqdex0s16id

View File

@@ -0,0 +1,243 @@
## @meta-authors Nicolò 'fenix' Santilio,
## @meta-version 2.5
##
## Referenced by [code]Firebase.Firestore[/code]. Represents the Firestore module.
## Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.
## Like Firebase Realtime Database, it keeps your data in sync across client apps through realtime listeners and offers offline support for mobile and web so you can build responsive apps that work regardless of network latency or Internet connectivity. Cloud Firestore also offers seamless integration with other Firebase and Google Cloud products, including Cloud Functions.
##
## Following Cloud Firestore's NoSQL data model, you store data in [b]documents[/b] that contain fields mapping to values. These documents are stored in [b]collections[/b], which are containers for your documents that you can use to organize your data and build queries.
## Documents support many different data types, from simple strings and numbers, to complex, nested objects. You can also create subcollections within documents and build hierarchical data structures that scale as your database grows.
## The Cloud Firestore data model supports whatever data structure works best for your app.
##
## (source: [url=https://firebase.google.com/docs/firestore]Firestore[/url])
##
## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore
@tool
class_name FirebaseFirestore
extends Node
const _API_VERSION : String = "v1"
## Emitted when a [code]list()[/code] or [code]query()[/code] request is [b]not[/b] successfully completed.
signal error(code, status, message)
enum Requests {
NONE = -1, ## Firestore is not processing any request.
LIST, ## Firestore is processing a [code]list()[/code] request checked a collection.
QUERY ## Firestore is processing a [code]query()[/code] request checked a collection.
}
# TODO: Implement cache size limit
const CACHE_SIZE_UNLIMITED = -1
const _CACHE_EXTENSION : String = ".fscache"
const _CACHE_RECORD_FILE : String = "RmlyZXN0b3JlIGNhY2hlLXJlY29yZHMu.fscache"
const _AUTHORIZATION_HEADER : String = "Authorization: Bearer "
const _MAX_POOLED_REQUEST_AGE = 30
## The code indicating the request Firestore is processing.
## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers.
## @enum Requests
var request: int = -1
## A Dictionary containing all authentication fields for the current logged user.
## @type Dictionary
var auth: Dictionary
var _config: Dictionary = {}
var _cache_loc: String
var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_unique_id()
var _base_url: String = ""
var _extended_url: String = "projects/[PROJECT_ID]/databases/(default)/documents/"
var _query_suffix: String = ":runQuery"
var _agg_query_suffix: String = ":runAggregationQuery"
#var _connect_check_node : HTTPRequest
var _request_list_node: HTTPRequest
var _requests_queue: Array = []
var _current_query: FirestoreQuery
## Returns a reference collection by its [i]path[/i].
##
## The returned object will be of [code]FirestoreCollection[/code] type.
## If saved into a variable, it can be used to issue requests checked the collection itself.
## @args path
## @return FirestoreCollection
func collection(path : String) -> FirestoreCollection:
for coll in get_children():
if coll is FirestoreCollection:
if coll.collection_name == path:
return coll
var coll : FirestoreCollection = FirestoreCollection.new()
coll._extended_url = _extended_url
coll._base_url = _base_url
coll._config = _config
coll.auth = auth
coll.collection_name = path
add_child(coll)
return coll
## Issue a query checked your Firestore database.
##
## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query.
## When awaited, this function returns the resulting array from the query.
##
## ex.
## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code]
##
## [b]Warning:[/b] It currently does not work offline!
##
## @args query
## @arg-types FirestoreQuery
## @return Array[FirestoreDocument]
func query(query : FirestoreQuery) -> Array:
if query.aggregations.size() > 0:
Firebase._printerr("Aggregation query sent with normal query call: " + str(query))
return []
var task : FirestoreTask = FirestoreTask.new()
task.action = FirestoreTask.Task.TASK_QUERY
var body: Dictionary = { structuredQuery = query.query }
var url: String = _base_url + _extended_url + query.sub_collection_path + _query_suffix
task.data = query
task._fields = JSON.stringify(body)
task._url = url
_pooled_request(task)
return await _handle_task_finished(task)
## Issue an aggregation query (sum, average, count) against your Firestore database;
## cheaper than a normal query and counting (for instance) values directly.
##
## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query.
## When awaited, this function returns the result from the aggregation query.
##
## ex.
## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code]
##
## [b]Warning:[/b] It currently does not work offline!
##
## @args query
## @arg-types FirestoreQuery
## @return Variant representing the array results of the aggregation query
func aggregation_query(query : FirestoreQuery) -> Variant:
if query.aggregations.size() == 0:
Firebase._printerr("Aggregation query sent with no aggregation values: " + str(query))
return 0
var task : FirestoreTask = FirestoreTask.new()
task.action = FirestoreTask.Task.TASK_AGG_QUERY
var body: Dictionary = { structuredAggregationQuery = { structuredQuery = query.query, aggregations = query.aggregations } }
var url: String = _base_url + _extended_url + _agg_query_suffix
task.data = query
task._fields = JSON.stringify(body)
task._url = url
_pooled_request(task)
var result = await _handle_task_finished(task)
return result
## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return an Array[FirestoreDocument]
## @args collection_id, page_size, page_token, order_by
## @arg-types String, int, String, String
## @arg-defaults , 0, "", ""
## @return Array[FirestoreDocument]
func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> Array:
var task : FirestoreTask = FirestoreTask.new()
task.action = FirestoreTask.Task.TASK_LIST
var url : String = _base_url + _extended_url + path
if page_size != 0:
url+="?pageSize="+str(page_size)
if page_token != "":
url+="&pageToken="+page_token
if order_by != "":
url+="&orderBy="+order_by
task.data = [path, page_size, page_token, order_by]
task._url = url
_pooled_request(task)
return await _handle_task_finished(task)
func _set_config(config_json : Dictionary) -> void:
_config = config_json
_cache_loc = _config["cacheLocation"]
_extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId)
# Since caching is causing a lot of issues, I'm removing this check for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted.
_check_emulating()
func _check_emulating() -> void :
## Check emulating
if not Firebase.emulating:
_base_url = "https://firestore.googleapis.com/{version}/".format({ version = _API_VERSION })
else:
var port : String = _config.emulators.ports.firestore
if port == "":
Firebase._printerr("You are in 'emulated' mode, but the port for Firestore has not been configured.")
else:
_base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port })
func _pooled_request(task : FirestoreTask) -> void:
if (auth == null or auth.is_empty()) and not Firebase.emulating:
Firebase._print("Unauthenticated request issued...")
Firebase.Auth.login_anonymous()
var result : Array = await Firebase.Auth.auth_request
if result[0] != 1:
_check_auth_error(result[0], result[1])
Firebase._print("Client connected as Anonymous")
if not Firebase.emulating:
task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken])
var http_request = HTTPRequest.new()
http_request.timeout = 5
Utilities.fix_http_request(http_request)
add_child(http_request)
http_request.request_completed.connect(
func(result, response_code, headers, body):
task._on_request_completed(result, response_code, headers, body)
http_request.queue_free()
)
http_request.request(task._url, task._headers, task._method, task._fields)
func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void:
auth = auth_result
for coll in get_children():
if coll is FirestoreCollection:
coll.auth = auth
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
auth = auth_result
for coll in get_children():
if coll is FirestoreCollection:
coll.auth = auth
func _on_FirebaseAuth_logout() -> void:
auth = {}
func _check_auth_error(code : int, message : String) -> void:
var err : String
match code:
400: err = "Please enable the Anonymous Sign-in method, or Authenticate the Client before issuing a request"
Firebase._printerr(err)
Firebase._printerr(message)
func _handle_task_finished(task : FirestoreTask):
await task.task_finished
if task.error.keys().size() > 0:
error.emit(task.error)
return task.data

View File

@@ -0,0 +1 @@
uid://c2n2dkjpnwcsd

View File

@@ -0,0 +1,178 @@
## @meta-authors TODO
## @meta-authors TODO
## @meta-version 2.3
## A reference to a Firestore Collection.
## Documentation TODO.
@tool
class_name FirestoreCollection
extends Node
signal error(error_result)
const _AUTHORIZATION_HEADER : String = "Authorization: Bearer "
const _separator : String = "/"
const _query_tag : String = "?"
const _documentId_tag : String = "documentId="
var auth : Dictionary
var collection_name : String
var _base_url : String
var _extended_url : String
var _config : Dictionary
var _documents := {}
# ----------------------- Requests
## @args document_id
## @return FirestoreTask
## used to GET a document from the collection, specify @document_id
func get_doc(document_id : String, from_cache : bool = false, is_listener : bool = false) -> FirestoreDocument:
if from_cache:
# for now, just return the child directly; in the future, make it smarter so there's a default, if long, polling time for this
for child in get_children():
if child.doc_name == document_id:
return child
var task : FirestoreTask = FirestoreTask.new()
task.action = FirestoreTask.Task.TASK_GET
task.data = collection_name + "/" + document_id
var url = _get_request_url() + _separator + document_id.replace(" ", "%20")
_process_request(task, document_id, url)
var result = await Firebase.Firestore._handle_task_finished(task)
if result != null:
for child in get_children():
if child.doc_name == document_id:
child.replace(result, true)
result = child
break
else:
print("get_document returned null for %s %s" % [collection_name, document_id])
return result
## @args document_id, fields
## @arg-defaults , {}
## @return FirestoreDocument
## used to ADD a new document to the collection, specify @documentID and @data
func add(document_id : String, data : Dictionary = {}) -> FirestoreDocument:
var task : FirestoreTask = FirestoreTask.new()
task.action = FirestoreTask.Task.TASK_POST
task.data = collection_name + "/" + document_id
var url = _get_request_url() + _query_tag + _documentId_tag + document_id
_process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data)))
var result = await Firebase.Firestore._handle_task_finished(task)
if result != null:
for child in get_children():
if child.doc_name == document_id:
child.free() # Consider throwing an error for this since it shouldn't already exist
break
result.collection_name = collection_name
add_child(result, true)
return result
## @args document
## @return FirestoreDocument
# used to UPDATE a document, specify the document
func update(document : FirestoreDocument) -> FirestoreDocument:
var task : FirestoreTask = FirestoreTask.new()
task.action = FirestoreTask.Task.TASK_PATCH
task.data = collection_name + "/" + document.doc_name
var url = _get_request_url() + _separator + document.doc_name.replace(" ", "%20") + "?"
for key in document.keys():
url+="updateMask.fieldPaths={key}&".format({key = key})
url = url.rstrip("&")
for key in document.keys():
if document.get_value(key) == null:
document._erase(key)
var temp_transforms
if document._transforms != null:
temp_transforms = document._transforms
document._transforms = null
var body = JSON.stringify({"fields": document.document})
_process_request(task, document.doc_name, url, body)
var result = await Firebase.Firestore._handle_task_finished(task)
if result != null:
for child in get_children():
if child.doc_name == result.doc_name:
child.replace(result, true)
break
if temp_transforms != null:
result._transforms = temp_transforms
return result
## @args document
## @return Dictionary
# Used to commit changes from transforms, specify the document with the transforms
func commit(document : FirestoreDocument) -> Dictionary:
var task : FirestoreTask = FirestoreTask.new()
task.action = FirestoreTask.Task.TASK_COMMIT
var url = get_database_url("commit")
document._transforms.set_config(
{
"extended_url": _extended_url,
"collection_name": collection_name
}
) # Only place we can set this is here, oofness
var body = document._transforms.serialize()
document.clear_field_transforms()
_process_request(task, document.doc_name, url, JSON.stringify(body))
return await Firebase.Firestore._handle_task_finished(task) # Not implementing the follow-up get here as user may have a listener that's already listening for changes, but user should call get if they don't
## @args document_id
## @return FirestoreTask
# used to DELETE a document, specify the document
func delete(document : FirestoreDocument) -> bool:
var doc_name = document.doc_name
var task : FirestoreTask = FirestoreTask.new()
task.action = FirestoreTask.Task.TASK_DELETE
task.data = document.collection_name + "/" + doc_name
var url = _get_request_url() + _separator + doc_name.replace(" ", "%20")
_process_request(task, doc_name, url)
var result = await Firebase.Firestore._handle_task_finished(task)
# Clean up the cache
if result:
for node in get_children():
if node.doc_name == doc_name:
node.free() # Should be only one
break
return result
func _get_request_url() -> String:
return _base_url + _extended_url + collection_name
func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void:
if auth == null or auth.is_empty():
Firebase._print("Unauthenticated request issued...")
Firebase.Auth.login_anonymous()
var result : Array = await Firebase.Auth.auth_request
if result[0] != 1:
Firebase.Firestore._check_auth_error(result[0], result[1])
return
Firebase._print("Client authenticated as Anonymous User.")
task._url = url
task._fields = fields
task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken])
Firebase.Firestore._pooled_request(task)
func get_database_url(append) -> String:
return _base_url + _extended_url.rstrip("/") + ":" + append

View File

@@ -0,0 +1 @@
uid://bnvqqpj5cima0

View File

@@ -0,0 +1,185 @@
## @meta-authors Kyle Szklenski
## @meta-version 2.2
## A reference to a Firestore Document.
## Documentation TODO.
@tool
class_name FirestoreDocument
extends Node
# A FirestoreDocument objects that holds all important values for a Firestore Document,
# @doc_name = name of the Firestore Document, which is the request PATH
# @doc_fields = fields held by Firestore Document, in APIs format
# created when requested from a `collection().get()` call
var document : Dictionary # the Document itself
var doc_name : String # only .name
var create_time : String # createTime
var collection_name : String # Name of the collection to which it belongs
var _transforms : FieldTransformArray # The transforms to apply
signal changed(changes)
func _init(doc : Dictionary = {}):
_transforms = FieldTransformArray.new()
if doc.has("fields"):
document = doc.fields
if doc.has("name"):
doc_name = doc.name
if doc_name.count("/") > 2:
doc_name = (doc_name.split("/") as Array).back()
if doc.has("createTime"):
self.create_time = doc.createTime
func replace(with : FirestoreDocument, is_listener := false) -> void:
var current = document.duplicate()
document = with.document
var changes = {
"added": [], "removed": [], "updated": [], "is_listener": is_listener
}
for key in current.keys():
if not document.has(key):
changes.removed.push_back({ "key" : key })
else:
var new_value = Utilities.from_firebase_type(document[key])
var old_value = Utilities.from_firebase_type(current[key])
if typeof(new_value) != typeof(old_value) or new_value != old_value:
if old_value == null:
changes.removed.push_back({ "key" : key }) # ??
else:
changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value })
for key in document.keys():
if not current.has(key):
changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) })
if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()):
changed.emit(changes)
func new_document(base_document: Dictionary) -> void:
var current = document.duplicate()
document = {}
for key in base_document.keys():
document[key] = Utilities.to_firebase_type(key)
var changes = {
"added": [], "removed": [], "updated": [], "is_listener": false
}
for key in current.keys():
if not document.has(key):
changes.removed.push_back({ "key" : key })
else:
var new_value = Utilities.from_firebase_type(document[key])
var old_value = Utilities.from_firebase_type(current[key])
if typeof(new_value) != typeof(old_value) or new_value != old_value:
if old_value == null:
changes.removed.push_back({ "key" : key }) # ??
else:
changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value })
for key in document.keys():
if not current.has(key):
changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) })
if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()):
changed.emit(changes)
func is_null_value(key) -> bool:
return document.has(key) and Utilities.from_firebase_type(document[key]) == null
# As of right now, we do not track these with track changes; instead, they'll come back when the document updates from the server.
# Until that time, it's expected if you want to track these types of changes that you commit for the transforms and then get the document yourself.
func add_field_transform(transform : FieldTransform) -> void:
_transforms.push_back(transform)
func remove_field_transform(transform : FieldTransform) -> void:
_transforms.erase(transform)
func clear_field_transforms() -> void:
_transforms.transforms.clear()
func remove_field(field_path : String) -> void:
if document.has(field_path):
document[field_path] = Utilities.to_firebase_type(null)
var changes = {
"added": [], "removed": [], "updated": [], "is_listener": false
}
changes.removed.push_back({ "key" : field_path })
changed.emit(changes)
func _erase(field_path : String) -> void:
document.erase(field_path)
func add_or_update_field(field_path : String, value : Variant) -> void:
var changes = {
"added": [], "removed": [], "updated": [], "is_listener": false
}
var existing_value = get_value(field_path)
var has_field_path = existing_value != null and not is_null_value(field_path)
var converted_value = Utilities.to_firebase_type(value)
document[field_path] = converted_value
if has_field_path:
changes.updated.push_back({ "key" : field_path, "old" : existing_value, "new" : value })
else:
changes.added.push_back({ "key" : field_path, "new" : value })
changed.emit(changes)
func on_snapshot(when_called : Callable, poll_time : float = 1.0) -> FirestoreListener.FirestoreListenerConnection:
if get_child_count() >= 1: # Only one listener per
assert(false, "Multiple listeners not allowed for the same document yet")
return
changed.connect(when_called, CONNECT_REFERENCE_COUNTED)
var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate()
add_child(listener)
listener.initialize_listener(collection_name, doc_name, poll_time)
listener.owner = self
var result = listener.enable_connection()
return result
func get_value(property : StringName) -> Variant:
if property == "doc_name":
return doc_name
elif property == "collection_name":
return collection_name
elif property == "create_time":
return create_time
if document.has(property):
var result = Utilities.from_firebase_type(document[property])
return result
return null
func _get(property: StringName) -> Variant:
return get_value(property)
func _set(property: StringName, value: Variant) -> bool:
assert(value != null, "When using the dictionary setter, the value cannot be null; use erase_field instead.")
document[property] = Utilities.to_firebase_type(value)
return true
func get_unsafe_document() -> Dictionary:
var result = {}
for key in keys():
result[key] = Utilities.from_firebase_type(document[key])
return result
func keys():
return document.keys()
# Call print(document) to return directly this document formatted
func _to_string() -> String:
return ("doc_name: {doc_name}, \ndata: {data}, \ncreate_time: {create_time}\n").format(
{doc_name = self.doc_name,
data = document,
create_time = self.create_time})

View File

@@ -0,0 +1 @@
uid://lvx6e1rnbjha

View File

@@ -0,0 +1,47 @@
class_name FirestoreListener
extends Node
const MinPollTime = 60 * 2 # seconds, so 2 minutes
var _doc_name : String
var _poll_time : float
var _collection : FirestoreCollection
var _total_time = 0.0
var _enabled := false
func initialize_listener(collection_name : String, doc_name : String, poll_time : float) -> void:
_poll_time = max(poll_time, MinPollTime)
_doc_name = doc_name
_collection = Firebase.Firestore.collection(collection_name)
func enable_connection() -> FirestoreListenerConnection:
_enabled = true
set_process(true)
return FirestoreListenerConnection.new(self)
func _process(delta: float) -> void:
if _enabled:
_total_time += delta
if _total_time >= _poll_time:
_check_for_server_updates()
_total_time = 0.0
func _check_for_server_updates() -> void:
var executor = func():
var doc = await _collection.get_doc(_doc_name, false, true)
if doc == null:
set_process(false) # Document was deleted out from under us, so stop updating
executor.call() # Hack to work around the await here, otherwise would have to call with await in _process and that's no bueno
class FirestoreListenerConnection extends RefCounted:
var connection
func _init(connection_node):
connection = connection_node
func stop():
if connection != null and is_instance_valid(connection):
connection.set_process(false)
connection.free()

View File

@@ -0,0 +1 @@
uid://bnn8dx3q452pr

View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://bwv7vtgssc0n5"]
[ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore_listener.gd" id="1_qlaei"]
[node name="FirestoreListener" type="Node"]
script = ExtResource("1_qlaei")

View File

@@ -0,0 +1,255 @@
## @meta-authors Nicoló 'fenix' Santilio, Kyle Szklenski
## @meta-version 1.4
## A firestore query.
## Documentation TODO.
@tool
extends RefCounted
class_name FirestoreQuery
class Order:
var obj: Dictionary
class Cursor:
var values: Array
var before: bool
func _init(v : Array,b : bool):
values = v
before = b
signal query_result(query_result)
const TEMPLATE_QUERY: Dictionary = {
select = {},
from = [],
where = {},
orderBy = [],
startAt = {},
endAt = {},
offset = 0,
limit = 0
}
var query: Dictionary = {}
var aggregations: Array[Dictionary] = []
var sub_collection_path: String = ""
enum OPERATOR {
# Standard operators
OPERATOR_UNSPECIFIED,
LESS_THAN,
LESS_THAN_OR_EQUAL,
GREATER_THAN,
GREATER_THAN_OR_EQUAL,
EQUAL,
NOT_EQUAL,
ARRAY_CONTAINS,
ARRAY_CONTAINS_ANY,
IN,
NOT_IN,
# Unary operators
IS_NAN,
IS_NULL,
IS_NOT_NAN,
IS_NOT_NULL,
# Complex operators
AND,
OR
}
enum DIRECTION {
DIRECTION_UNSPECIFIED,
ASCENDING,
DESCENDING
}
# Select which fields you want to return as a reflection from your query.
# Fields must be added inside a list. Only a field is accepted inside the list
# Leave the Array empty if you want to return the whole document
func select(fields) -> FirestoreQuery:
match typeof(fields):
TYPE_STRING:
query["select"] = { fields = { fieldPath = fields } }
TYPE_ARRAY:
for field in fields:
field = ({ fieldPath = field })
query["select"] = { fields = fields }
_:
print("Type of 'fields' is not accepted.")
return self
# Select the collection you want to return the query result from
# if @all_descendants also sub-collections will be returned. If false, only documents will be returned
func from(collection_id : String, all_descendants : bool = true) -> FirestoreQuery:
query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}]
return self
# @collections_array MUST be an Array of Arrays with this structure
# [ ["collection_id", true/false] ]
func from_many(collections_array : Array) -> FirestoreQuery:
var collections : Array = []
for collection in collections_array:
collections.append({collectionId = collection[0], allDescendants = collection[1]})
query["from"] = collections.duplicate(true)
return self
# Query the value of a field you want to match
# @field : the name of the field
# @operator : from FirestoreQuery.OPERATOR
# @value : can be any type - String, int, bool, float
# @chain : from FirestoreQuery.OPERATOR.[OR/AND], use it only if you want to chain "AND" or "OR" logic with futher where() calls
# eg. super.where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20)
func where(field : String, operator : int, value = null, chain : int = -1):
if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]:
if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")):
var filters : Array = []
if query.has("where") and query.where.has("compositeFilter"):
if chain == -1:
filters = query.where.compositeFilter.filters.duplicate(true)
chain = OPERATOR.get(query.where.compositeFilter.op)
else:
filters.append(query.where)
filters.append(create_unary_filter(field, operator))
query["where"] = create_composite_filter(chain, filters)
else:
query["where"] = create_unary_filter(field, operator)
else:
if value == null:
print("A value must be defined to match the field: {field}".format({field = field}))
else:
if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")):
var filters : Array = []
if query.has("where") and query.where.has("compositeFilter"):
if chain == -1:
filters = query.where.compositeFilter.filters.duplicate(true)
chain = OPERATOR.get(query.where.compositeFilter.op)
else:
filters.append(query.where)
filters.append(create_field_filter(field, operator, value))
query["where"] = create_composite_filter(chain, filters)
else:
query["where"] = create_field_filter(field, operator, value)
return self
# Order by a field, defining its name and the order direction
# default directoin = Ascending
func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> FirestoreQuery:
query["orderBy"] = [_order_object(field, direction).obj]
return self
# Order by a set of fields and directions
# @order_list is an Array of Arrays with the following structure
# [@field_name , @DIRECTION.[direction]]
# else, order_object() can be called to return an already parsed Dictionary
func order_by_fields(order_field_list : Array) -> FirestoreQuery:
var order_list : Array = []
for order in order_field_list:
if order is Array:
order_list.append(_order_object(order[0], order[1]).obj)
elif order is Order:
order_list.append(order.obj)
query["orderBy"] = order_list
return self
func start_at(value, before : bool) -> FirestoreQuery:
var cursor : Cursor = _cursor_object(value, before)
query["startAt"] = { values = cursor.values, before = cursor.before }
print(query["startAt"])
return self
func end_at(value, before : bool) -> FirestoreQuery:
var cursor : Cursor = _cursor_object(value, before)
query["startAt"] = { values = cursor.values, before = cursor.before }
print(query["startAt"])
return self
func offset(offset : int) -> FirestoreQuery:
if offset < 0:
print("If specified, offset must be >= 0")
else:
query["offset"] = offset
return self
func limit(limit : int) -> FirestoreQuery:
if limit < 0:
print("If specified, offset must be >= 0")
else:
query["limit"] = limit
return self
func aggregate() -> FirestoreAggregation:
return FirestoreAggregation.new(self)
class FirestoreAggregation extends RefCounted:
var _query: FirestoreQuery
func _init(query: FirestoreQuery) -> void:
_query = query
func sum(field: String) -> FirestoreQuery:
_query.aggregations.push_back({ sum = { field = { fieldPath = field }}})
return _query
func count(up_to: int) -> FirestoreQuery:
_query.aggregations.push_back({ count = { upTo = up_to }})
return _query
func average(field: String) -> FirestoreQuery:
_query.aggregations.push_back({ avg = { field = { fieldPath = field }}})
return _query
# UTILITIES ----------------------------------------
static func _cursor_object(value, before : bool) -> Cursor:
var parse : Dictionary = Utilities.dict2fields({value = value}).fields.value
var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before)
return cursor
static func _order_object(field : String, direction : int) -> Order:
var order : Order = Order.new()
order.obj = { field = { fieldPath = field }, direction = DIRECTION.keys()[direction] }
return order
func create_field_filter(field : String, operator : int, value) -> Dictionary:
return {
fieldFilter = {
field = { fieldPath = field },
op = OPERATOR.keys()[operator],
value = Utilities.dict2fields({value = value}).fields.value
} }
func create_unary_filter(field : String, operator : int) -> Dictionary:
return {
unaryFilter = {
field = { fieldPath = field },
op = OPERATOR.keys()[operator],
} }
func create_composite_filter(operator : int, filters : Array) -> Dictionary:
return {
compositeFilter = {
op = OPERATOR.keys()[operator],
filters = filters
} }
func clean() -> void:
query = { }
func _to_string() -> String:
var pretty : String = "QUERY:\n"
for key in query.keys():
pretty += "- {key} = {value}\n".format({key = key, value = query.get(key)})
return pretty

View File

@@ -0,0 +1 @@
uid://c7laxjcm52kh5

View File

@@ -0,0 +1,188 @@
## @meta-authors Nicolò 'fenix' Santilio, Kyle 'backat50ft' Szklenski
## @meta-version 1.4
##
## A [code]FirestoreTask[/code] is an independent node inheriting [code]HTTPRequest[/code] that processes a [code]Firestore[/code] request.
## Once the Task is completed (both if successfully or not) it will emit the relative signal (or a general purpose signal [code]task_finished()[/code]) and will destroy automatically.
##
## Being a [code]Node[/code] it can be stored in a variable to yield checked it, and receive its result as a callback.
## All signals emitted by a [code]FirestoreTask[/code] represent a direct level of signal communication, which can be high ([code]get_document(document), result_query(result)[/code]) or low ([code]task_finished(result)[/code]).
## An indirect level of communication with Tasks is also provided, redirecting signals to the [class FirebaseFirestore] module.
##
## ex.
## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code]
## [code]var result : Array = await task.task_finished[/code]
## [code]var result : Array = await task.result_query[/code]
## [code]var result : Array = await Firebase.Firestore.task_finished[/code]
## [code]var result : Array = await Firebase.Firestore.result_query[/code]
##
## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask
@tool
class_name FirestoreTask
extends RefCounted
## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result.
## @arg-types Variant
signal task_finished()
enum Task {
TASK_GET, ## A GET Request Task, processing a get() request
TASK_POST, ## A POST Request Task, processing add() request
TASK_PATCH, ## A PATCH Request Task, processing a update() request
TASK_DELETE, ## A DELETE Request Task, processing a delete() request
TASK_QUERY, ## A POST Request Task, processing a query() request
TASK_AGG_QUERY, ## A POST Request Task, processing an aggregation_query() request
TASK_LIST, ## A POST Request Task, processing a list() request
TASK_COMMIT ## A POST Request Task that hits the write api
}
## Mapping of Task enum values to descriptions for use in printing user-friendly error codes.
const TASK_MAP = {
Task.TASK_GET: "GET DOCUMENT",
Task.TASK_POST: "ADD DOCUMENT",
Task.TASK_PATCH: "UPDATE DOCUMENT",
Task.TASK_DELETE: "DELETE DOCUMENT",
Task.TASK_QUERY: "QUERY COLLECTION",
Task.TASK_LIST: "LIST DOCUMENTS",
Task.TASK_COMMIT: "COMMIT DOCUMENT",
Task.TASK_AGG_QUERY: "AGG QUERY COLLECTION"
}
## The code indicating the request Firestore is processing.
## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers.
## @setter set_action
var action : int = -1 : set = set_action
## A variable, temporary holding the result of the request.
var data
var error: Dictionary
var document: FirestoreDocument
var _response_headers: PackedStringArray = PackedStringArray()
var _response_code: int = 0
var _method: int = -1
var _url: String = ""
var _fields: String = ""
var _headers: PackedStringArray = []
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
var bod = body.get_string_from_utf8()
if bod != "":
bod = Utilities.get_json_data(bod)
var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK
# Probably going to regret this...
if response_code == HTTPClient.RESPONSE_OK:
match action:
Task.TASK_POST, Task.TASK_GET, Task.TASK_PATCH:
document = FirestoreDocument.new(bod)
data = document
Task.TASK_DELETE:
data = true
Task.TASK_QUERY:
data = []
for doc in bod:
if doc.has('document'):
data.append(FirestoreDocument.new(doc.document))
Task.TASK_AGG_QUERY:
var agg_results = []
for agg_result in bod:
var idx = 0
var query_results = {}
for field_value in agg_result.result.aggregateFields.keys():
var agg = data.aggregations[idx]
var field = agg_result.result.aggregateFields[field_value]
query_results[agg.keys()[0]] = Utilities.from_firebase_type(field)
idx += 1
agg_results.push_back(query_results)
data = agg_results
Task.TASK_LIST:
data = []
if bod.has('documents'):
for doc in bod.documents:
data.append(FirestoreDocument.new(doc))
if bod.has("nextPageToken"):
data.append(bod.nextPageToken)
Task.TASK_COMMIT:
data = bod # Commit's response is not a full document, so don't treat it as such
else:
var description = ""
if TASK_MAP.has(action):
description = "(" + TASK_MAP[action] + ")"
Firebase._printerr("Action in error was: " + str(action) + " " + description)
build_error(bod, action, description)
task_finished.emit()
func build_error(_error, action, description) -> void:
if _error:
if _error is Array and _error.size() > 0 and _error[0].has("error"):
_error = _error[0].error
elif _error is Dictionary and _error.keys().size() > 0 and _error.has("error"):
_error = _error.error
error = _error
else:
#error.code, error.status, error.message
error = { "error": {
"code": 0,
"status": "Unknown Error",
"message": "Error: %s - %s" % [action, description]
}
}
data = null
func set_action(value : int) -> void:
action = value
match action:
Task.TASK_GET, Task.TASK_LIST:
_method = HTTPClient.METHOD_GET
Task.TASK_POST, Task.TASK_QUERY, Task.TASK_AGG_QUERY:
_method = HTTPClient.METHOD_POST
Task.TASK_PATCH:
_method = HTTPClient.METHOD_PATCH
Task.TASK_DELETE:
_method = HTTPClient.METHOD_DELETE
Task.TASK_COMMIT:
_method = HTTPClient.METHOD_POST
_:
assert(false)
func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary:
var ret := dic_a.duplicate(true)
for key in dic_b:
var val = dic_b[key]
if val == null and nullify:
ret.erase(key)
elif val is Array:
ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val)
elif val is Dictionary:
ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val)
else:
ret[key] = val
return ret
func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array:
var ret := arr_a.duplicate(true)
ret.resize(len(arr_b))
var deletions := 0
for i in len(arr_b):
var index : int = i - deletions
var val = arr_b[index]
if val == null and nullify:
ret.remove_at(index)
deletions += i
elif val is Array:
ret[index] = _merge_array(ret[index] if ret[index] else [], val)
elif val is Dictionary:
ret[index] = _merge_dict(ret[index] if ret[index] else {}, val)
else:
ret[index] = val
return ret

View File

@@ -0,0 +1 @@
uid://bacq4pvag0wii

View File

@@ -0,0 +1,3 @@
class_name FirestoreTransform
extends RefCounted

View File

@@ -0,0 +1 @@
uid://pybqqmlkru0q

View File

@@ -0,0 +1,59 @@
## @meta-authors Nicolò 'fenix' Santilio,
## @meta-version 1.2
##
## ex.
## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code]
## [code]var result : Array = await task.task_finished[/code]
## [code]var result : Array = await task.result_query[/code]
## [code]var result : Array = await Firebase.Firestore.task_finished[/code]
## [code]var result : Array = await Firebase.Firestore.result_query[/code]
##
## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask
@tool
class_name FunctionTask
extends RefCounted
## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result.
## @arg-types Variant
signal task_finished(result)
## Emitted when a cloud function is correctly executed, returning the Response Code and Result Body
## @arg-types FirestoreDocument
signal function_executed(response, result)
## Emitted when a request is [b]not[/b] successfully completed.
## @arg-types Dictionary
signal task_error(code, status, message)
## A variable, temporary holding the result of the request.
var data: Dictionary
var error: Dictionary
## Whether the data came from cache.
var from_cache : bool = false
var _response_headers : PackedStringArray = PackedStringArray()
var _response_code : int = 0
var _method : int = -1
var _url : String = ""
var _fields : String = ""
var _headers : PackedStringArray = []
func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
var bod = Utilities.get_json_data(body)
if bod == null:
bod = {content = body.get_string_from_utf8()} # I don't understand what this line does at all. What the hell?!
var offline: bool = typeof(bod) == TYPE_NIL
from_cache = offline
data = bod
if response_code == HTTPClient.RESPONSE_OK and data!=null:
function_executed.emit(result, data)
else:
error = {result=result, response_code=response_code, data=data}
task_error.emit(result, response_code, str(data))
task_finished.emit(data)

View File

@@ -0,0 +1 @@
uid://xrp4ueej55sc

View File

@@ -0,0 +1,219 @@
## @meta-authors Nicolò 'fenix' Santilio,
## @meta-version 2.5
##
## (source: [url=https://firebase.google.com/docs/functions]Functions[/url])
##
## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Functions
@tool
class_name FirebaseFunctions
extends Node
## Emitted when a [code]query()[/code] request is successfully completed. [code]error()[/code] signal will be emitted otherwise.
## @arg-types Array
## Emitted when a [code]list()[/code] or [code]query()[/code] request is [b]not[/b] successfully completed.
signal task_error(code,status,message)
# TODO: Implement cache size limit
const CACHE_SIZE_UNLIMITED = -1
const _CACHE_EXTENSION : String = ".fscache"
const _CACHE_RECORD_FILE : String = "RmlyZXN0b3JlIGNhY2hlLXJlY29yZHMu.fscache"
const _AUTHORIZATION_HEADER : String = "Authorization: Bearer "
const _MAX_POOLED_REQUEST_AGE = 30
## The code indicating the request Firestore is processing.
## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers.
## @enum Requests
var request : int = -1
## Whether cache files can be used and generated.
## @default true
var persistence_enabled : bool = false
## Whether an internet connection can be used.
## @default true
var networking: bool = true : set = set_networking
## A Dictionary containing all authentication fields for the current logged user.
## @type Dictionary
var auth : Dictionary
var _config : Dictionary = {}
var _cache_loc: String
var _encrypt_key: String = "" if Utilities.is_web() else OS.get_unique_id()
var _base_url : String = ""
var _http_request_pool : Array = []
var _offline: bool = false : set = _set_offline
func _ready() -> void:
set_process(false)
func _process(delta : float) -> void:
for i in range(_http_request_pool.size() - 1, -1, -1):
var request = _http_request_pool[i]
if not request.get_meta("requesting"):
var lifetime: float = request.get_meta("lifetime") + delta
if lifetime > _MAX_POOLED_REQUEST_AGE:
request.queue_free()
_http_request_pool.remove_at(i)
return # Prevent setting a value on request after it's already been queue_freed
request.set_meta("lifetime", lifetime)
## @args
## @return FunctionTask
func execute(function: String, method: int, params: Dictionary = {}, body: Dictionary = {}) -> FunctionTask:
set_process(true)
var function_task : FunctionTask = FunctionTask.new()
function_task.task_error.connect(_on_task_error)
function_task.task_finished.connect(_on_task_finished)
function_task.function_executed.connect(_on_function_executed)
function_task._method = method
var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function
function_task._url = url
if not params.is_empty():
url += "?"
for key in params.keys():
url += key + "=" + params[key] + "&"
if not body.is_empty():
function_task._fields = JSON.stringify(body)
_pooled_request(function_task)
return function_task
func set_networking(value: bool) -> void:
if value:
enable_networking()
else:
disable_networking()
func enable_networking() -> void:
if networking:
return
networking = true
_base_url = _base_url.replace("storeoffline", "functions")
func disable_networking() -> void:
if not networking:
return
networking = false
# Pointing to an invalid url should do the trick.
_base_url = _base_url.replace("functions", "storeoffline")
func _set_offline(value: bool) -> void:
if value == _offline:
return
_offline = value
if not persistence_enabled:
return
return
func _set_config(config_json : Dictionary) -> void:
_config = config_json
_cache_loc = _config["cacheLocation"]
if _encrypt_key == "": _encrypt_key = _config.apiKey
_check_emulating()
func _check_emulating() -> void :
## Check emulating
if not Firebase.emulating:
_base_url = "https://{zone}-{projectId}.cloudfunctions.net/".format({ zone = _config.functionsGeoZone, projectId = _config.projectId })
else:
var port : String = _config.emulators.ports.functions
if port == "":
Firebase._printerr("You are in 'emulated' mode, but the port for Cloud Functions has not been configured.")
else:
_base_url = "http://localhost:{port}/{projectId}/{zone}/".format({ port = port, zone = _config.functionsGeoZone, projectId = _config.projectId })
func _pooled_request(task : FunctionTask) -> void:
if _offline:
task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray())
return
if auth == null or auth.is_empty():
Firebase._print("Unauthenticated request issued...")
Firebase.Auth.login_anonymous()
var result : Array = await Firebase.Auth.auth_request
if result[0] != 1:
_check_auth_error(result[0], result[1])
Firebase._print("Client connected as Anonymous")
task._headers = ["Content-Type: application/json", _AUTHORIZATION_HEADER + auth.idtoken]
var http_request : HTTPRequest
for request in _http_request_pool:
if not request.get_meta("requesting"):
http_request = request
break
if not http_request:
http_request = HTTPRequest.new()
Utilities.fix_http_request(http_request)
http_request.accept_gzip = false
_http_request_pool.append(http_request)
add_child(http_request)
http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request))
http_request.set_meta("requesting", true)
http_request.set_meta("lifetime", 0.0)
http_request.set_meta("task", task)
http_request.request(task._url, task._headers, task._method, task._fields)
# -------------
func _on_task_finished(data : Dictionary) :
pass
func _on_function_executed(result : int, data : Dictionary) :
pass
func _on_task_error(code : int, status : int, message : String):
task_error.emit(code, status, message)
Firebase._printerr(message)
func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void:
auth = auth_result
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
auth = auth_result
func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void:
request.get_meta("task")._on_request_completed(result, response_code, headers, body)
request.set_meta("requesting", false)
func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void:
_set_offline(result != HTTPRequest.RESULT_SUCCESS)
func _on_FirebaseAuth_logout() -> void:
auth = {}
func _check_auth_error(code : int, message : String) -> void:
var err : String
match code:
400: err = "Please, enable Anonymous Sign-in method or Authenticate the Client before issuing a request (best option)"
Firebase._printerr(err)

View File

@@ -0,0 +1 @@
uid://bil4t34fdutkf

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://2selq12fp4q0"
path="res://.godot/imported/icon.svg-5c4f39d37c9275a3768de73a392fd315.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/godot-firebase/icon.svg"
dest_files=["res://.godot/imported/icon.svg-5c4f39d37c9275a3768de73a392fd315.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,7 @@
[plugin]
name="GodotFirebase"
description="Google Firebase SDK written in GDScript for use in Godot Engine 4.0 projects."
author="GodotNutsOrg"
version="2.0"
script="plugin.gd"

View File

@@ -0,0 +1,8 @@
@tool
extends EditorPlugin
func _enter_tree() -> void:
add_autoload_singleton("Firebase", "res://addons/godot-firebase/firebase/firebase.tscn")
func _exit_tree() -> void:
remove_autoload_singleton("Firebase")

View File

@@ -0,0 +1 @@
uid://cu50owt435wxm

View File

@@ -0,0 +1,30 @@
class_name QueueableHTTPRequest
extends HTTPRequest
signal queue_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray)
var _queue := []
# Determine if we need to set Use Threads to true; it can cause collisions with get_http_client_status() due to a thread returning the data _after_ having checked the connection status and result in double-requests.
func _ready() -> void:
request_completed.connect(
func(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray):
queue_request_completed.emit(result, response_code, headers, body)
if not _queue.is_empty():
var req = _queue.pop_front()
self.request(req.url, req.headers, req.method, req.data)
)
func request(url : String, headers : PackedStringArray = PackedStringArray(), method := HTTPClient.METHOD_GET, data : String = "") -> Error:
var status = get_http_client_status()
var result = OK
if status != HTTPClient.STATUS_DISCONNECTED:
_queue.push_back({url=url, headers=headers, method=method, data=data})
return result
result = super.request(url, headers, method, data)
return result

View File

@@ -0,0 +1 @@
uid://37mycjbymm3y

View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://ctb4l7plg8kqg"]
[ext_resource type="Script" path="res://addons/godot-firebase/queues/queueable_http_request.gd" id="1_2rucc"]
[node name="QueueableHTTPRequest" type="HTTPRequest"]
script = ExtResource("1_2rucc")

View File

@@ -0,0 +1,36 @@
@tool
class_name FirebaseRemoteConfig
extends Node
const RemoteConfigFunctionId = "getRemoteConfig"
signal remote_config_received(config)
signal remote_config_error(error)
var _project_config = {}
var _headers : PackedStringArray = [
]
var _auth : Dictionary
func _set_config(config_json : Dictionary) -> void:
_project_config = config_json # This may get confusing, hoping the variable name makes it easier to understand
func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void:
_auth = auth_result
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
_auth = auth_result
func _on_FirebaseAuth_logout() -> void:
_auth = {}
func get_remote_config() -> void:
var function_task = Firebase.Functions.execute("getRemoteConfig", HTTPClient.METHOD_GET, {}, {}) as FunctionTask
var result = await function_task.task_finished
Firebase._print("Config request result: " + str(result))
if result.has("error"):
remote_config_error.emit(result)
return
var config = RemoteConfig.new(result)
remote_config_received.emit(config)

View File

@@ -0,0 +1 @@
uid://xqngri5s6yc5

View File

@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://5xa6ulbllkjk"]
[ext_resource type="Script" path="res://addons/godot-firebase/remote_config/firebase_remote_config.gd" id="1_wx4ds"]
[node name="FirebaseRemoteConfig" type="HTTPRequest"]
use_threads = true
script = ExtResource("1_wx4ds")

View File

@@ -0,0 +1,14 @@
class_name RemoteConfig
extends RefCounted
var default_config = {}
func _init(values : Dictionary) -> void:
default_config = values
func get_value(key : String) -> Variant:
if default_config.has(key):
return default_config[key]
Firebase._printerr("Remote config does not contain key: " + key)
return null

View File

@@ -0,0 +1 @@
uid://bqimgkjkven2l

View File

@@ -0,0 +1,362 @@
## @meta-authors SIsilicon
## @meta-version 2.2
## The Storage API for Firebase.
## This object handles all firebase storage tasks, variables and references. To use this API, you must first create a [StorageReference] with [method ref]. With the reference, you can then query and manipulate the file or folder in the cloud storage.
##
## [i]Note: In HTML builds, you must configure [url=https://firebase.google.com/docs/storage/web/download-files#cors_configuration]CORS[/url] in your storage bucket.[i]
@tool
class_name FirebaseStorage
extends Node
const _API_VERSION : String = "v0"
## @arg-types int, int, PackedStringArray
## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode
## Emitted when a [StorageTask] has finished with an error.
signal task_failed(result, response_code, data)
## The current storage bucket the Storage API is referencing.
var bucket : String
## @default false
## Whether a task is currently being processed.
var requesting : bool = false
var _auth : Dictionary
var _config : Dictionary
var _references : Dictionary = {}
var _base_url : String = ""
var _extended_url : String = "/[API_VERSION]/b/[APP_ID]/o/[FILE_PATH]"
var _root_ref : StorageReference
var _http_client : HTTPClient = HTTPClient.new()
var _pending_tasks : Array = []
var _current_task : StorageTask
var _response_code : int
var _response_headers : PackedStringArray
var _response_data : PackedByteArray
var _content_length : int
var _reading_body : bool
func _notification(what : int) -> void:
if what == NOTIFICATION_INTERNAL_PROCESS:
_internal_process(get_process_delta_time())
func _internal_process(_delta : float) -> void:
if not requesting:
set_process_internal(false)
return
var task = _current_task
match _http_client.get_status():
HTTPClient.STATUS_DISCONNECTED:
_http_client.connect_to_host(_base_url, 443, TLSOptions.client()) # Uhh, check if this is going to work. I assume not.
HTTPClient.STATUS_RESOLVING, \
HTTPClient.STATUS_REQUESTING, \
HTTPClient.STATUS_CONNECTING:
_http_client.poll()
HTTPClient.STATUS_CONNECTED:
var err := _http_client.request_raw(task._method, task._url, task._headers, task.data)
if err:
_finish_request(HTTPRequest.RESULT_CONNECTION_ERROR)
HTTPClient.STATUS_BODY:
if _http_client.has_response() or _reading_body:
_reading_body = true
# If there is a response...
if _response_headers.is_empty():
_response_headers = _http_client.get_response_headers() # Get response headers.
_response_code = _http_client.get_response_code()
for header in _response_headers:
if "Content-Length" in header:
_content_length = header.trim_prefix("Content-Length: ").to_int()
break
_http_client.poll()
var chunk = _http_client.read_response_body_chunk() # Get a chunk.
if chunk.size() == 0:
# Got nothing, wait for buffers to fill a bit.
pass
else:
_response_data += chunk # Append to read buffer.
if _content_length != 0:
task.progress = float(_response_data.size()) / _content_length
if _http_client.get_status() != HTTPClient.STATUS_BODY:
task.progress = 1.0
_finish_request(HTTPRequest.RESULT_SUCCESS)
else:
task.progress = 1.0
_finish_request(HTTPRequest.RESULT_SUCCESS)
HTTPClient.STATUS_CANT_CONNECT:
_finish_request(HTTPRequest.RESULT_CANT_CONNECT)
HTTPClient.STATUS_CANT_RESOLVE:
_finish_request(HTTPRequest.RESULT_CANT_RESOLVE)
HTTPClient.STATUS_CONNECTION_ERROR:
_finish_request(HTTPRequest.RESULT_CONNECTION_ERROR)
HTTPClient.STATUS_TLS_HANDSHAKE_ERROR:
_finish_request(HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR)
## @args path
## @arg-defaults ""
## @return StorageReference
## Returns a reference to a file or folder in the storage bucket. It's this reference that should be used to control the file/folder checked the server end.
func ref(path := "") -> StorageReference:
if _config == null or _config.is_empty():
return null
# Create a root storage reference if there's none
# and we're not making one.
if path != "" and not _root_ref:
_root_ref = ref()
path = _simplify_path(path)
if not _references.has(path):
var ref := StorageReference.new()
_references[path] = ref
ref.bucket = bucket
ref.full_path = path
ref.file_name = path.get_file()
ref.parent = ref(path.path_join(".."))
ref.root = _root_ref
ref.storage = self
add_child(ref)
return ref
else:
return _references[path]
func _set_config(config_json : Dictionary) -> void:
_config = config_json
if bucket != _config.storageBucket:
bucket = _config.storageBucket
_http_client.close()
_check_emulating()
func _check_emulating() -> void :
## Check emulating
if not Firebase.emulating:
_base_url = "https://firebasestorage.googleapis.com"
else:
var port : String = _config.emulators.ports.storage
if port == "":
Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.")
else:
_base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port })
func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> Variant:
if _is_invalid_authentication():
Firebase._printerr("Error uploading to storage: Invalid authentication")
return 0
var task := StorageTask.new()
task.ref = ref
task._url = _get_file_url(ref)
task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD
task._headers = headers
task.data = data
_process_request(task)
return await task.task_finished
func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Variant:
if _is_invalid_authentication():
Firebase._printerr("Error downloading from storage: Invalid authentication")
return 0
var info_task := StorageTask.new()
info_task.ref = ref
info_task._url = _get_file_url(ref)
info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META
_process_request(info_task)
if url_only or meta_only:
return await info_task.task_finished
var task := StorageTask.new()
task.ref = ref
task._url = _get_file_url(ref) + "?alt=media&token="
task.action = StorageTask.Task.TASK_DOWNLOAD
_pending_tasks.append(task)
var data = await info_task.task_finished
if info_task.result == OK:
task._url += info_task.data.downloadTokens
else:
task.data = info_task.data
task.response_headers = info_task.response_headers
task.response_code = info_task.response_code
task.result = info_task.result
task.finished = true
task.task_finished.emit(null)
task_failed.emit(task.result, task.response_code, task.data)
_pending_tasks.erase(task)
return null
return await task.task_finished
func _list(ref : StorageReference, list_all : bool) -> Array:
if _is_invalid_authentication():
Firebase._printerr("Error getting object list from storage: Invalid authentication")
return []
var task := StorageTask.new()
task.ref = ref
task._url = _get_file_url(_root_ref).trim_suffix("/")
task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST
_process_request(task)
return await task.task_finished
func _delete(ref : StorageReference) -> bool:
if _is_invalid_authentication():
Firebase._printerr("Error deleting object from storage: Invalid authentication")
return false
var task := StorageTask.new()
task.ref = ref
task._url = _get_file_url(ref)
task.action = StorageTask.Task.TASK_DELETE
_process_request(task)
var data = await task.task_finished
return data == null
func _process_request(task : StorageTask) -> void:
if requesting:
_pending_tasks.append(task)
return
requesting = true
var headers = Array(task._headers)
headers.append("Authorization: Bearer " + _auth.idtoken)
task._headers = PackedStringArray(headers)
_current_task = task
_response_code = 0
_response_headers = PackedStringArray()
_response_data = PackedByteArray()
_content_length = 0
_reading_body = false
if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]:
_http_client.close()
set_process_internal(true)
func _finish_request(result : int) -> void:
var task := _current_task
requesting = false
task.result = result
task.response_code = _response_code
task.response_headers = _response_headers
match task.action:
StorageTask.Task.TASK_DOWNLOAD:
task.data = _response_data
StorageTask.Task.TASK_DELETE:
_references.erase(task.ref.full_path)
for child in get_children():
if child.full_path == task.ref.full_path:
child.queue_free()
break
if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY:
task.data = null
StorageTask.Task.TASK_DOWNLOAD_URL:
var json = Utilities.get_json_data(_response_data)
if json != null and json.has("error"):
Firebase._printerr("Error getting object download url: "+json["error"].message)
if json != null and json.has("downloadTokens"):
task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens
else:
task.data = ""
StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL:
var json = Utilities.get_json_data(_response_data)
var items := []
if json != null and json.has("error"):
Firebase._printerr("Error getting data from storage: "+json["error"].message)
if json != null and json.has("items"):
for item in json.items:
var item_name : String = item.name
if item.bucket != bucket:
continue
if not item_name.begins_with(task.ref.full_path):
continue
if task.action == StorageTask.Task.TASK_LIST:
var dir_path : Array = item_name.split("/")
var slash_count : int = task.ref.full_path.count("/")
item_name = ""
for i in slash_count + 1:
item_name += dir_path[i]
if i != slash_count and slash_count != 0:
item_name += "/"
if item_name in items:
continue
items.append(item_name)
task.data = items
_:
var json = Utilities.get_json_data(_response_data)
task.data = json
var next_task = _get_next_pending_task()
task.finished = true
task.task_finished.emit(task.data) # I believe this parameter has been missing all along, but not sure. Caused weird results at times with a yield/await returning null, but the task containing data.
if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"):
task_failed.emit(task.result, task.response_code, task.data)
if next_task and not next_task.finished:
_process_request(next_task)
func _get_next_pending_task() -> StorageTask:
if _pending_tasks.is_empty():
return null
return _pending_tasks.pop_front()
func _get_file_url(ref : StorageReference) -> String:
var url := _extended_url.replace("[APP_ID]", ref.bucket)
url = url.replace("[API_VERSION]", _API_VERSION)
return url.replace("[FILE_PATH]", ref.full_path.uri_encode())
# Removes any "../" or "./" in the file path.
func _simplify_path(path : String) -> String:
var dirs := path.split("/")
var new_dirs := []
for dir in dirs:
if dir == "..":
new_dirs.pop_back()
elif dir == ".":
pass
else:
new_dirs.push_back(dir)
var new_path := "/".join(PackedStringArray(new_dirs))
new_path = new_path.replace("//", "/")
new_path = new_path.replace("\\", "/")
return new_path
func _on_FirebaseAuth_login_succeeded(auth_token : Dictionary) -> void:
_auth = auth_token
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
_auth = auth_result
func _on_FirebaseAuth_logout() -> void:
_auth = {}
func _is_invalid_authentication() -> bool:
return (_config == null or _config.is_empty()) or (_auth == null or _auth.is_empty())

View File

@@ -0,0 +1 @@
uid://vxab4cp6e8hx

View File

@@ -0,0 +1,159 @@
## @meta-authors SIsilicon
## @meta-version 2.2
## A reference to a file or folder in the Firebase cloud storage.
## This object is used to interact with the cloud storage. You may get data from the server, as well as upload your own back to it.
@tool
class_name StorageReference
extends Node
## The default MIME type to use when uploading a file.
## Data sent with this type are interpreted as plain binary data. Note that firebase will generate an MIME type based checked the file extenstion if none is provided.
const DEFAULT_MIME_TYPE = "application/octet-stream"
## A dictionary of common MIME types based checked a file extension.
## Example: [code]MIME_TYPES.png[/code] will return [code]image/png[/code].
const MIME_TYPES = {
"bmp": "image/bmp",
"css": "text/css",
"csv": "text/csv",
"gd": "text/plain",
"htm": "text/html",
"html": "text/html",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"json": "application/json",
"mp3": "audio/mpeg",
"mpeg": "video/mpeg",
"ogg": "audio/ogg",
"ogv": "video/ogg",
"png": "image/png",
"shader": "text/plain",
"svg": "image/svg+xml",
"tif": "image/tiff",
"tiff": "image/tiff",
"tres": "text/plain",
"tscn": "text/plain",
"txt": "text/plain",
"wav": "audio/wav",
"webm": "video/webm",
"webp": "image/webp",
"xml": "text/xml",
}
## @default ""
## The stroage bucket this referenced file/folder is located in.
var bucket : String = ""
## @default ""
## The path to the file/folder relative to [member bucket].
var full_path : String = ""
## @default ""
## The name of the file/folder, including any file extension.
## Example: If the [member full_path] is [code]images/user/image.png[/code], then the [member name] would be [code]image.png[/code].
var file_name : String = ""
## The parent [StorageReference] one level up the file hierarchy.
## If the current [StorageReference] is the root (i.e. the [member full_path] is [code]""[/code]) then the [member parent] will be [code]null[/code].
var parent : StorageReference
## The root [StorageReference].
var root : StorageReference
## @type FirebaseStorage
## The Storage API that created this [StorageReference] to begin with.
var storage # FirebaseStorage (Can't static type due to cyclic reference)
## @args path
## @return StorageReference
## Returns a reference to another [StorageReference] relative to this one.
func child(path : String) -> StorageReference:
return storage.ref(full_path.path_join(path))
## @args data, metadata
## @return int
## Makes an attempt to upload data to the referenced file location. Returns Variant
func put_data(data : PackedByteArray, metadata := {}) -> Variant:
if not "Content-Length" in metadata and not Utilities.is_web():
metadata["Content-Length"] = data.size()
var headers := []
for key in metadata:
headers.append("%s: %s" % [key, metadata[key]])
return await storage._upload(data, headers, self, false)
## @args data, metadata
## @return int
## Like [method put_data], but [code]data[/code] is a [String].
func put_string(data : String, metadata := {}) -> Variant:
return await put_data(data.to_utf8_buffer(), metadata)
## @args file_path, metadata
## @return int
## Like [method put_data], but the data comes from a file at [code]file_path[/code].
func put_file(file_path : String, metadata := {}) -> Variant:
var file := FileAccess.open(file_path, FileAccess.READ)
var data := file.get_buffer(file.get_length())
if "Content-Type" in metadata:
metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE)
return await put_data(data, metadata)
## @return Variant
## Makes an attempt to download the files from the referenced file location. Status checked this task is found in the returned [StorageTask].
func get_data() -> Variant:
var result = await storage._download(self, false, false)
return result
## @return StorageTask
## Like [method get_data], but the data in the returned [StorageTask] comes in the form of a [String].
func get_string() -> String:
var task := await get_data()
_on_task_finished(task, "stringify")
return task.data
## @return StorageTask
## Attempts to get the download url that points to the referenced file's data. Using the url directly may require an authentication header. Status checked this task is found in the returned [StorageTask].
func get_download_url() -> Variant:
return await storage._download(self, false, true)
## @return StorageTask
## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask].
func get_metadata() -> Variant:
return await storage._download(self, true, false)
## @args metadata
## @return StorageTask
## Attempts to update the metadata of the referenced file. Any field with a value of [code]null[/code] will be deleted checked the server end. Status checked this task is found in the returned [StorageTask].
func update_metadata(metadata : Dictionary) -> Variant:
var data := JSON.stringify(metadata).to_utf8_buffer()
var headers := PackedStringArray(["Accept: application/json"])
return await storage._upload(data, headers, self, true)
## @return StorageTask
## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status checked this task is found in the returned [StorageTask].
func list() -> Array:
return await storage._list(self, false)
## @return StorageTask
## Attempts to get the list of files and/or folders under the referenced folder This function is nested unlike [method list]. Status checked this task is found in the returned [StorageTask].
func list_all() -> Array:
return await storage._list(self, true)
## @return StorageTask
## Attempts to delete the referenced file/folder. If successful, the reference will become invalid And can no longer be used. If you need to reference this location again, make a new reference with [method StorageTask.ref]. Status checked this task is found in the returned [StorageTask].
func delete() -> bool:
return await storage._delete(self)
func _to_string() -> String:
var string := "gs://%s/%s" % [bucket, full_path]
return string
func _on_task_finished(task : StorageTask, action : String) -> void:
match action:
"stringify":
if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY:
task.data = task.data.get_string_from_utf8()

View File

@@ -0,0 +1 @@
uid://bpvs4eelnej5i

View File

@@ -0,0 +1,74 @@
## @meta-authors SIsilicon, Kyle 'backat50ft' Szklenski
## @meta-version 2.2
## An object that keeps track of an operation performed by [StorageReference].
@tool
class_name StorageTask
extends RefCounted
enum Task {
TASK_UPLOAD,
TASK_UPLOAD_META,
TASK_DOWNLOAD,
TASK_DOWNLOAD_META,
TASK_DOWNLOAD_URL,
TASK_LIST,
TASK_LIST_ALL,
TASK_DELETE,
TASK_MAX ## The number of [enum Task] constants.
}
## Emitted when the task is finished. Returns data depending checked the success and action of the task.
signal task_finished(data)
## Boolean to determine if this request involves metadata only
var is_meta : bool
## @enum Task
## @default -1
## @setter set_action
## The kind of operation this [StorageTask] is keeping track of.
var action : int = -1 : set = set_action
var ref # Should not be needed, damnit
## @default PackedByteArray()
## Data that the tracked task will/has returned.
var data = PackedByteArray() # data can be of any type.
## @default 0.0
## The percentage of data that has been received.
var progress : float = 0.0
## @default -1
## @enum HTTPRequest.Result
## The resulting status of the task. Anyting other than [constant HTTPRequest.RESULT_SUCCESS] means an error has occured.
var result : int = -1
## @default false
## Whether the task is finished processing.
var finished : bool = false
## @default PackedStringArray()
## The returned HTTP response headers.
var response_headers := PackedStringArray()
## @default 0
## @enum HTTPClient.ResponseCode
## The returned HTTP response code.
var response_code : int = 0
var _method : int = -1
var _url : String = ""
var _headers : PackedStringArray = PackedStringArray()
func set_action(value : int) -> void:
action = value
match action:
Task.TASK_UPLOAD:
_method = HTTPClient.METHOD_POST
Task.TASK_UPLOAD_META:
_method = HTTPClient.METHOD_PATCH
Task.TASK_DELETE:
_method = HTTPClient.METHOD_DELETE
_:
_method = HTTPClient.METHOD_GET

View File

@@ -0,0 +1 @@
uid://bfw61qsheaqjr