added firebase and rudimentary leaderboard support
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=19 format=3 uid="uid://sa1d1rftyn87"]
|
[gd_scene load_steps=20 format=3 uid="uid://sa1d1rftyn87"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://civx1w876wd7n" path="res://scripts/main_menu.gd" id="1_haaol"]
|
[ext_resource type="Script" uid="uid://civx1w876wd7n" path="res://scripts/main_menu.gd" id="1_haaol"]
|
||||||
[ext_resource type="PackedScene" uid="uid://dpootbr7qgac1" path="res://Tools/playlist_generator.tscn" id="2_2rg1o"]
|
[ext_resource type="PackedScene" uid="uid://dpootbr7qgac1" path="res://Tools/playlist_generator.tscn" id="2_2rg1o"]
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
[ext_resource type="Texture2D" uid="uid://bt6utik8unkxa" path="res://assets/Textures/ObjectTextures/money.png" id="7_ia0hc"]
|
[ext_resource type="Texture2D" uid="uid://bt6utik8unkxa" path="res://assets/Textures/ObjectTextures/money.png" id="7_ia0hc"]
|
||||||
[ext_resource type="Resource" uid="uid://cl5m2v6fntob7" path="res://LevelResources/EnemyWorkingScene.tres" id="7_m04lp"]
|
[ext_resource type="Resource" uid="uid://cl5m2v6fntob7" path="res://LevelResources/EnemyWorkingScene.tres" id="7_m04lp"]
|
||||||
[ext_resource type="Resource" uid="uid://c0116tnaxhe2r" path="res://LevelResources/Highwire.tres" id="8_1thib"]
|
[ext_resource type="Resource" uid="uid://c0116tnaxhe2r" path="res://LevelResources/Highwire.tres" id="8_1thib"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b6nt3p1kntjod" path="res://UI/authentication.tscn" id="17_m04lp"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_a5tps"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_a5tps"]
|
||||||
shader = ExtResource("2_hyw7c")
|
shader = ExtResource("2_hyw7c")
|
||||||
@@ -165,6 +166,9 @@ placeholder_text = "Leaderboard Name"
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
icon = ExtResource("7_ia0hc")
|
icon = ExtResource("7_ia0hc")
|
||||||
|
|
||||||
|
[node name="Authentication" parent="." instance=ExtResource("17_m04lp")]
|
||||||
|
layout_mode = 1
|
||||||
|
|
||||||
[connection signal="item_selected" from="MarginContainer/VBoxContainer/HBoxContainer/OptionButton" to="." method="_on_option_button_item_selected"]
|
[connection signal="item_selected" from="MarginContainer/VBoxContainer/HBoxContainer/OptionButton" to="." method="_on_option_button_item_selected"]
|
||||||
[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/Add Leaderboard" to="." method="_on_add_leaderboard_pressed"]
|
[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/Add Leaderboard" to="." method="_on_add_leaderboard_pressed"]
|
||||||
[connection signal="pressed" from="MarginContainer/VBoxContainer/Continue" to="." method="_on_continue_pressed"]
|
[connection signal="pressed" from="MarginContainer/VBoxContainer/Continue" to="." method="_on_continue_pressed"]
|
||||||
|
|||||||
66
UI/authentication.tscn
Normal file
66
UI/authentication.tscn
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://b6nt3p1kntjod"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cbmnj1dfgfwp5" path="res://scripts/authentication.gd" id="1_bgnbp"]
|
||||||
|
[ext_resource type="Theme" uid="uid://clek42ofxr45f" path="res://DefaultTheme.tres" id="2_hbvo2"]
|
||||||
|
|
||||||
|
[node name="Authentication" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_bgnbp")
|
||||||
|
|
||||||
|
[node name="ColorRect" type="ColorRect" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
color = Color(0, 0, 0, 1)
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -441.0
|
||||||
|
offset_top = -351.5
|
||||||
|
offset_right = 441.0
|
||||||
|
offset_bottom = 351.5
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme = ExtResource("2_hbvo2")
|
||||||
|
theme_override_constants/separation = 34
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="StateLabel" type="Label" parent="VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Not Logged In"
|
||||||
|
|
||||||
|
[node name="email_edit" type="LineEdit" parent="VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Email"
|
||||||
|
|
||||||
|
[node name="password_edit" type="LineEdit" parent="VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Password"
|
||||||
|
secret = true
|
||||||
|
|
||||||
|
[node name="log_in" type="Button" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Log In"
|
||||||
|
|
||||||
|
[node name="sign_up" type="Button" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Sign Up"
|
||||||
|
|
||||||
|
[connection signal="pressed" from="VBoxContainer/log_in" to="." method="_on_log_in_pressed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/sign_up" to="." method="_on_sign_up_pressed"]
|
||||||
24
addons/godot-firebase/.env
Normal file
24
addons/godot-firebase/.env
Normal 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=""
|
||||||
21
addons/godot-firebase/LICENSE
Normal file
21
addons/godot-firebase/LICENSE
Normal 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.
|
||||||
3
addons/godot-firebase/README.md
Normal file
3
addons/godot-firebase/README.md
Normal 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).
|
||||||
346
addons/godot-firebase/Utilities.gd
Normal file
346
addons/godot-firebase/Utilities.gd
Normal 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)
|
||||||
1
addons/godot-firebase/Utilities.gd.uid
Normal file
1
addons/godot-firebase/Utilities.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cggx7ysauq6p0
|
||||||
693
addons/godot-firebase/auth/auth.gd
Normal file
693
addons/godot-firebase/auth/auth.gd
Normal 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">🔥 You can close this window now. 🔥</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)
|
||||||
1
addons/godot-firebase/auth/auth.gd.uid
Normal file
1
addons/godot-firebase/auth/auth.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bvlvihyfqktvr
|
||||||
32
addons/godot-firebase/auth/auth_provider.gd
Normal file
32
addons/godot-firebase/auth/auth_provider.gd
Normal 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 ""
|
||||||
1
addons/godot-firebase/auth/auth_provider.gd.uid
Normal file
1
addons/godot-firebase/auth/auth_provider.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://i5o82uhvlsyj
|
||||||
21
addons/godot-firebase/auth/providers/facebook.gd
Normal file
21
addons/godot-firebase/auth/providers/facebook.gd
Normal 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"
|
||||||
|
|
||||||
|
|
||||||
1
addons/godot-firebase/auth/providers/facebook.gd.uid
Normal file
1
addons/godot-firebase/auth/providers/facebook.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://s5c12vma7ufy
|
||||||
14
addons/godot-firebase/auth/providers/github.gd
Normal file
14
addons/godot-firebase/auth/providers/github.gd
Normal 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"
|
||||||
1
addons/godot-firebase/auth/providers/github.gd.uid
Normal file
1
addons/godot-firebase/auth/providers/github.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://gactahg7liyw
|
||||||
13
addons/godot-firebase/auth/providers/google.gd
Normal file
13
addons/godot-firebase/auth/providers/google.gd
Normal 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"
|
||||||
1
addons/godot-firebase/auth/providers/google.gd.uid
Normal file
1
addons/godot-firebase/auth/providers/google.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dkl7y6dl8fg8s
|
||||||
39
addons/godot-firebase/auth/providers/twitter.gd
Normal file
39
addons/godot-firebase/auth/providers/twitter.gd
Normal 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)
|
||||||
1
addons/godot-firebase/auth/providers/twitter.gd.uid
Normal file
1
addons/godot-firebase/auth/providers/twitter.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bwvku5unmemyb
|
||||||
44
addons/godot-firebase/auth/user_data.gd
Normal file
44
addons/godot-firebase/auth/user_data.gd
Normal 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
|
||||||
1
addons/godot-firebase/auth/user_data.gd.uid
Normal file
1
addons/godot-firebase/auth/user_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d1njxyqnur3a1
|
||||||
51
addons/godot-firebase/database/database.gd
Normal file
51
addons/godot-firebase/database/database.gd
Normal 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
|
||||||
1
addons/godot-firebase/database/database.gd.uid
Normal file
1
addons/godot-firebase/database/database.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ba2spgo4jo2qw
|
||||||
109
addons/godot-firebase/database/database_store.gd
Normal file
109
addons/godot-firebase/database/database_store.gd
Normal 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]
|
||||||
1
addons/godot-firebase/database/database_store.gd.uid
Normal file
1
addons/godot-firebase/database/database_store.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bo3i7q8s2bfmq
|
||||||
@@ -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")
|
||||||
@@ -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"]
|
||||||
124
addons/godot-firebase/database/once_reference.gd
Normal file
124
addons/godot-firebase/database/once_reference.gd
Normal 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)
|
||||||
1
addons/godot-firebase/database/once_reference.gd.uid
Normal file
1
addons/godot-firebase/database/once_reference.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d0b2x1kc1w1w3
|
||||||
176
addons/godot-firebase/database/reference.gd
Normal file
176
addons/godot-firebase/database/reference.gd
Normal 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()
|
||||||
1
addons/godot-firebase/database/reference.gd.uid
Normal file
1
addons/godot-firebase/database/reference.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ofyy1lc3qlfn
|
||||||
16
addons/godot-firebase/database/resource.gd
Normal file
16
addons/godot-firebase/database/resource.gd
Normal 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})
|
||||||
1
addons/godot-firebase/database/resource.gd.uid
Normal file
1
addons/godot-firebase/database/resource.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cbqame2gc2atr
|
||||||
109
addons/godot-firebase/dynamiclinks/dynamiclinks.gd
Normal file
109
addons/godot-firebase/dynamiclinks/dynamiclinks.gd
Normal 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 = {}
|
||||||
1
addons/godot-firebase/dynamiclinks/dynamiclinks.gd.uid
Normal file
1
addons/godot-firebase/dynamiclinks/dynamiclinks.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dgbj7nixmfyrs
|
||||||
144
addons/godot-firebase/firebase/firebase.gd
Normal file
144
addons/godot-firebase/firebase/firebase.gd
Normal 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))
|
||||||
1
addons/godot-firebase/firebase/firebase.gd.uid
Normal file
1
addons/godot-firebase/firebase/firebase.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c0vsvs7ol6n0x
|
||||||
36
addons/godot-firebase/firebase/firebase.tscn
Normal file
36
addons/godot-firebase/firebase/firebase.tscn
Normal 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
|
||||||
22
addons/godot-firebase/firestore/field_transform.gd
Normal file
22
addons/godot-firebase/firestore/field_transform.gd
Normal 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]
|
||||||
1
addons/godot-firebase/firestore/field_transform.gd.uid
Normal file
1
addons/godot-firebase/firestore/field_transform.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dw7p8101sxern
|
||||||
35
addons/godot-firebase/firestore/field_transform_array.gd
Normal file
35
addons/godot-firebase/firestore/field_transform_array.gd
Normal 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bopmc0mi55dvu
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bit26sxq4daw7
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c5gx3a3dsmwop
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://xf5c8b0lrjpl
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cei7mxm5uqrkc
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cqkqdex0s16id
|
||||||
243
addons/godot-firebase/firestore/firestore.gd
Normal file
243
addons/godot-firebase/firestore/firestore.gd
Normal 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
|
||||||
1
addons/godot-firebase/firestore/firestore.gd.uid
Normal file
1
addons/godot-firebase/firestore/firestore.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c2n2dkjpnwcsd
|
||||||
178
addons/godot-firebase/firestore/firestore_collection.gd
Normal file
178
addons/godot-firebase/firestore/firestore_collection.gd
Normal 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bnvqqpj5cima0
|
||||||
185
addons/godot-firebase/firestore/firestore_document.gd
Normal file
185
addons/godot-firebase/firestore/firestore_document.gd
Normal 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})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://lvx6e1rnbjha
|
||||||
47
addons/godot-firebase/firestore/firestore_listener.gd
Normal file
47
addons/godot-firebase/firestore/firestore_listener.gd
Normal 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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bnn8dx3q452pr
|
||||||
6
addons/godot-firebase/firestore/firestore_listener.tscn
Normal file
6
addons/godot-firebase/firestore/firestore_listener.tscn
Normal 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")
|
||||||
255
addons/godot-firebase/firestore/firestore_query.gd
Normal file
255
addons/godot-firebase/firestore/firestore_query.gd
Normal 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
|
||||||
1
addons/godot-firebase/firestore/firestore_query.gd.uid
Normal file
1
addons/godot-firebase/firestore/firestore_query.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c7laxjcm52kh5
|
||||||
188
addons/godot-firebase/firestore/firestore_task.gd
Normal file
188
addons/godot-firebase/firestore/firestore_task.gd
Normal 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
|
||||||
1
addons/godot-firebase/firestore/firestore_task.gd.uid
Normal file
1
addons/godot-firebase/firestore/firestore_task.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bacq4pvag0wii
|
||||||
3
addons/godot-firebase/firestore/firestore_transform.gd
Normal file
3
addons/godot-firebase/firestore/firestore_transform.gd
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class_name FirestoreTransform
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://pybqqmlkru0q
|
||||||
59
addons/godot-firebase/functions/function_task.gd
Normal file
59
addons/godot-firebase/functions/function_task.gd
Normal 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)
|
||||||
1
addons/godot-firebase/functions/function_task.gd.uid
Normal file
1
addons/godot-firebase/functions/function_task.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://xrp4ueej55sc
|
||||||
219
addons/godot-firebase/functions/functions.gd
Normal file
219
addons/godot-firebase/functions/functions.gd
Normal 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)
|
||||||
1
addons/godot-firebase/functions/functions.gd.uid
Normal file
1
addons/godot-firebase/functions/functions.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bil4t34fdutkf
|
||||||
1
addons/godot-firebase/icon.svg
Normal file
1
addons/godot-firebase/icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 4.9 KiB |
37
addons/godot-firebase/icon.svg.import
Normal file
37
addons/godot-firebase/icon.svg.import
Normal 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
|
||||||
7
addons/godot-firebase/plugin.cfg
Normal file
7
addons/godot-firebase/plugin.cfg
Normal 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"
|
||||||
8
addons/godot-firebase/plugin.gd
Normal file
8
addons/godot-firebase/plugin.gd
Normal 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")
|
||||||
1
addons/godot-firebase/plugin.gd.uid
Normal file
1
addons/godot-firebase/plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cu50owt435wxm
|
||||||
30
addons/godot-firebase/queues/queueable_http_request.gd
Normal file
30
addons/godot-firebase/queues/queueable_http_request.gd
Normal 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://37mycjbymm3y
|
||||||
6
addons/godot-firebase/queues/queueable_http_request.tscn
Normal file
6
addons/godot-firebase/queues/queueable_http_request.tscn
Normal 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")
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://xqngri5s6yc5
|
||||||
@@ -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")
|
||||||
14
addons/godot-firebase/remote_config/remote_config.gd
Normal file
14
addons/godot-firebase/remote_config/remote_config.gd
Normal 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
|
||||||
1
addons/godot-firebase/remote_config/remote_config.gd.uid
Normal file
1
addons/godot-firebase/remote_config/remote_config.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bqimgkjkven2l
|
||||||
362
addons/godot-firebase/storage/storage.gd
Normal file
362
addons/godot-firebase/storage/storage.gd
Normal 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())
|
||||||
1
addons/godot-firebase/storage/storage.gd.uid
Normal file
1
addons/godot-firebase/storage/storage.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://vxab4cp6e8hx
|
||||||
159
addons/godot-firebase/storage/storage_reference.gd
Normal file
159
addons/godot-firebase/storage/storage_reference.gd
Normal 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()
|
||||||
1
addons/godot-firebase/storage/storage_reference.gd.uid
Normal file
1
addons/godot-firebase/storage/storage_reference.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bpvs4eelnej5i
|
||||||
74
addons/godot-firebase/storage/storage_task.gd
Normal file
74
addons/godot-firebase/storage/storage_task.gd
Normal 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
|
||||||
1
addons/godot-firebase/storage/storage_task.gd.uid
Normal file
1
addons/godot-firebase/storage/storage_task.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bfw61qsheaqjr
|
||||||
129
addons/http-sse-client/HTTPSSEClient.gd
Normal file
129
addons/http-sse-client/HTTPSSEClient.gd
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
@tool
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal new_sse_event(headers, event, data)
|
||||||
|
signal connected
|
||||||
|
signal connection_error(error)
|
||||||
|
|
||||||
|
const event_tag = "event:"
|
||||||
|
const data_tag = "data:"
|
||||||
|
const continue_internal = "continue_internal"
|
||||||
|
|
||||||
|
var httpclient = HTTPClient.new()
|
||||||
|
var is_connected = false
|
||||||
|
|
||||||
|
var domain
|
||||||
|
var url_after_domain
|
||||||
|
var port
|
||||||
|
var trusted_chain
|
||||||
|
var common_name_override
|
||||||
|
var told_to_connect = false
|
||||||
|
var connection_in_progress = false
|
||||||
|
var is_requested = false
|
||||||
|
var response_body = PackedByteArray()
|
||||||
|
|
||||||
|
func connect_to_host(domain : String, url_after_domain : String, port : int = -1, trusted_chain : X509Certificate = null, common_name_override : String = ""):
|
||||||
|
process_mode = Node.PROCESS_MODE_INHERIT
|
||||||
|
self.domain = domain
|
||||||
|
self.url_after_domain = url_after_domain
|
||||||
|
self.port = port
|
||||||
|
self.trusted_chain = trusted_chain
|
||||||
|
self.common_name_override = common_name_override
|
||||||
|
told_to_connect = true
|
||||||
|
|
||||||
|
func attempt_to_connect():
|
||||||
|
var tls_options = TLSOptions.client(trusted_chain, common_name_override)
|
||||||
|
var err = httpclient.connect_to_host(domain, port, tls_options)
|
||||||
|
if err == OK:
|
||||||
|
connected.emit()
|
||||||
|
is_connected = true
|
||||||
|
else:
|
||||||
|
connection_error.emit(str(err))
|
||||||
|
|
||||||
|
func attempt_to_request(httpclient_status):
|
||||||
|
if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING:
|
||||||
|
return
|
||||||
|
|
||||||
|
if httpclient_status == HTTPClient.STATUS_CONNECTED:
|
||||||
|
var err = httpclient.request(HTTPClient.METHOD_POST, url_after_domain, ["Accept: text/event-stream"])
|
||||||
|
if err == OK:
|
||||||
|
is_requested = true
|
||||||
|
|
||||||
|
func _parse_response_body(headers):
|
||||||
|
var body = response_body.get_string_from_utf8()
|
||||||
|
if body:
|
||||||
|
var event_data = get_event_data(body)
|
||||||
|
if event_data.event != "keep-alive" and event_data.event != continue_internal:
|
||||||
|
var result = Utilities.get_json_data(event_data.data)
|
||||||
|
if result != null:
|
||||||
|
var parsed_text = result
|
||||||
|
if response_body.size() > 0: # stop here if the value doesn't parse
|
||||||
|
response_body.resize(0)
|
||||||
|
new_sse_event.emit(headers, event_data.event, result)
|
||||||
|
else:
|
||||||
|
if event_data.event != continue_internal:
|
||||||
|
response_body.resize(0)
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
if !told_to_connect:
|
||||||
|
return
|
||||||
|
|
||||||
|
if !is_connected:
|
||||||
|
if !connection_in_progress:
|
||||||
|
attempt_to_connect()
|
||||||
|
connection_in_progress = true
|
||||||
|
return
|
||||||
|
|
||||||
|
httpclient.poll()
|
||||||
|
var httpclient_status = httpclient.get_status()
|
||||||
|
if !is_requested:
|
||||||
|
attempt_to_request(httpclient_status)
|
||||||
|
return
|
||||||
|
|
||||||
|
if httpclient.has_response() or httpclient_status == HTTPClient.STATUS_BODY:
|
||||||
|
var headers = httpclient.get_response_headers_as_dictionary()
|
||||||
|
|
||||||
|
if httpclient_status == HTTPClient.STATUS_BODY:
|
||||||
|
httpclient.poll()
|
||||||
|
var chunk = httpclient.read_response_body_chunk()
|
||||||
|
if(chunk.size() == 0):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
response_body = response_body + chunk
|
||||||
|
|
||||||
|
_parse_response_body(headers)
|
||||||
|
|
||||||
|
elif Firebase.emulating and Firebase._config.workarounds.database_connection_closed_issue:
|
||||||
|
# Emulation does not send the close connection header currently, so we need to manually read the response body
|
||||||
|
# see issue https://github.com/firebase/firebase-tools/issues/3329 in firebase-tools
|
||||||
|
# also comment https://github.com/GodotNuts/GodotFirebase/issues/154#issuecomment-831377763 which explains the issue
|
||||||
|
while httpclient.connection.get_available_bytes():
|
||||||
|
var data = httpclient.connection.get_partial_data(1)
|
||||||
|
if data[0] == OK:
|
||||||
|
response_body.append_array(data[1])
|
||||||
|
if response_body.size() > 0:
|
||||||
|
_parse_response_body(headers)
|
||||||
|
|
||||||
|
func get_event_data(body : String):
|
||||||
|
var result = {}
|
||||||
|
var event_idx = body.find(event_tag)
|
||||||
|
if event_idx == -1:
|
||||||
|
result["event"] = continue_internal
|
||||||
|
return result
|
||||||
|
assert(event_idx != -1)
|
||||||
|
var data_idx = body.find(data_tag, event_idx + event_tag.length())
|
||||||
|
assert(data_idx != -1)
|
||||||
|
var event = body.substr(event_idx, data_idx)
|
||||||
|
var event_value = event.replace(event_tag, "").strip_edges()
|
||||||
|
assert(event_value)
|
||||||
|
assert(event_value.length() > 0)
|
||||||
|
result["event"] = event_value
|
||||||
|
var data = body.right(body.length() - (data_idx + data_tag.length())).strip_edges()
|
||||||
|
assert(data)
|
||||||
|
assert(data.length() > 0)
|
||||||
|
result["data"] = data
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
if httpclient:
|
||||||
|
httpclient.close()
|
||||||
1
addons/http-sse-client/HTTPSSEClient.gd.uid
Normal file
1
addons/http-sse-client/HTTPSSEClient.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cdxp41ctxh5eb
|
||||||
6
addons/http-sse-client/HTTPSSEClient.tscn
Normal file
6
addons/http-sse-client/HTTPSSEClient.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=2]
|
||||||
|
|
||||||
|
[ext_resource path="res://addons/http-sse-client/HTTPSSEClient.gd" type="Script" id=1]
|
||||||
|
|
||||||
|
[node name="HTTPSSEClient" type="Node"]
|
||||||
|
script = ExtResource( 1 )
|
||||||
21
addons/http-sse-client/LICENSE
Normal file
21
addons/http-sse-client/LICENSE
Normal 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.
|
||||||
16
addons/http-sse-client/README.md
Normal file
16
addons/http-sse-client/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# HTTPSSEClient
|
||||||
|
|
||||||
|
This is an implementation of the server-sent events/event-source protocol (https://www.w3.org/TR/eventsource/) in GDScript for the Godot game engine.
|
||||||
|
|
||||||
|
To use this, simply download this project and place it into the `res://addons/HTTPSSEClient/` folder in your project; then you can just turn it on.
|
||||||
|
|
||||||
|
I've included Demo.tscn and Demo.gd to show the usage of this plugin, and here's a summary:
|
||||||
|
|
||||||
|
1) Download and place into the proper folder as the above suggests
|
||||||
|
2) Switch the new plugin, found in Project Settings -> Plugins, to active
|
||||||
|
3) Instantiate a new HTTPSSEClient node in your scene tree somewhere
|
||||||
|
4) Click on the script icon for the newly-created node
|
||||||
|
5) Enter in any connection information necessary to connect to your SSE-supported server; for demonstration purposes, I use Firebase, and in the config dictionary, I just add the entire config I get back from adding a new Android app to any Firebase project (it'll give you back the google-services.json file, copy/paste it into config and change the url in the script to firebase_url and you're set for this)
|
||||||
|
6) If you're using Firebase, you need a sub_url value that is something like "/your_demo_list.json?auth=" and then the value of either your Firebase ID token, or your database secret. It's not clear how long database secrets will remain functional as they're already deprecated, but it is supported for the time being due to backward compatibility issues.
|
||||||
|
|
||||||
|
When using my GDFirebase plugin, all of the above is handled for you automatically, so you will only need to use the information provided by that plugin.
|
||||||
8
addons/http-sse-client/httpsseclient_plugin.gd
Normal file
8
addons/http-sse-client/httpsseclient_plugin.gd
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
func _enter_tree():
|
||||||
|
add_custom_type("HTTPSSEClient", "Node", preload("HTTPSSEClient.gd"), preload("icon.png"))
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
remove_custom_type("HTTPSSEClient")
|
||||||
1
addons/http-sse-client/httpsseclient_plugin.gd.uid
Normal file
1
addons/http-sse-client/httpsseclient_plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://l8ynb8chj2cg
|
||||||
BIN
addons/http-sse-client/icon.png
Normal file
BIN
addons/http-sse-client/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 B |
34
addons/http-sse-client/icon.png.import
Normal file
34
addons/http-sse-client/icon.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cr4uf04actcj5"
|
||||||
|
path="res://.godot/imported/icon.png-fd61c0c5f1d1b4df37e0abe3d8f80985.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/http-sse-client/icon.png"
|
||||||
|
dest_files=["res://.godot/imported/icon.png-fd61c0c5f1d1b4df37e0abe3d8f80985.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
|
||||||
7
addons/http-sse-client/plugin.cfg
Normal file
7
addons/http-sse-client/plugin.cfg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="HTTPSSEClient"
|
||||||
|
description="An HTTPClient-based implementation that supports server-sent events, effectively enabling push notifications in Godot, using GDScript."
|
||||||
|
author="Kyle Szklenski"
|
||||||
|
version="1.0"
|
||||||
|
script="httpsseclient_plugin.gd"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user