├── addons ├── godot-firebase │ ├── LICENSE │ ├── README.md │ ├── Utilities.gd │ ├── auth │ │ ├── auth.gd │ │ ├── auth_provider.gd │ │ ├── providers │ │ │ ├── facebook.gd │ │ │ ├── github.gd │ │ │ ├── google.gd │ │ │ └── twitter.gd │ │ └── user_data.gd │ ├── database │ │ ├── database.gd │ │ ├── database_store.gd │ │ ├── firebase_database_reference.tscn │ │ ├── firebase_once_database_reference.tscn │ │ ├── once_reference.gd │ │ ├── reference.gd │ │ └── resource.gd │ ├── dynamiclinks │ │ └── dynamiclinks.gd │ ├── example.env │ ├── firebase │ │ ├── firebase.gd │ │ └── firebase.tscn │ ├── firestore │ │ ├── field_transform.gd │ │ ├── field_transform_array.gd │ │ ├── field_transforms │ │ │ ├── decrement_transform.gd │ │ │ ├── increment_transform.gd │ │ │ ├── max_transform.gd │ │ │ ├── min_transform.gd │ │ │ └── server_timestamp_transform.gd │ │ ├── firestore.gd │ │ ├── firestore_collection.gd │ │ ├── firestore_document.gd │ │ ├── firestore_listener.gd │ │ ├── firestore_listener.tscn │ │ ├── firestore_query.gd │ │ ├── firestore_task.gd │ │ └── firestore_transform.gd │ ├── functions │ │ ├── function_task.gd │ │ └── functions.gd │ ├── icon.svg │ ├── icon.svg.import │ ├── plugin.cfg │ ├── plugin.gd │ ├── queues │ │ ├── queueable_http_request.gd │ │ └── queueable_http_request.tscn │ ├── remote_config │ │ ├── firebase_remote_config.gd │ │ ├── firebase_remote_config.tscn │ │ └── remote_config.gd │ └── storage │ │ ├── storage.gd │ │ ├── storage_reference.gd │ │ └── storage_task.gd └── http-sse-client │ ├── HTTPSSEClient.gd │ ├── HTTPSSEClient.tscn │ ├── LICENSE │ ├── README.md │ ├── httpsseclient_plugin.gd │ ├── icon.png │ └── plugin.cfg └── ios_plugins └── godot_svc ├── bin 3.x ├── godot_svc.debug.xcframework │ ├── Info.plist │ ├── ios-arm64_armv7 │ │ └── godot_svc-device.release_debug.a │ └── ios-arm64_x86_64-simulator │ │ └── godot_svc-simulator.release_debug.a ├── godot_svc.gdip └── godot_svc.release.xcframework │ ├── Info.plist │ ├── ios-arm64_armv7 │ └── godot_svc-device.release.a │ └── ios-arm64_x86_64-simulator │ └── godot_svc-simulator.release.a └── src ├── godot_svc.gdip ├── godot_svc.h ├── godot_svc.mm ├── godot_svc_delegate.mm ├── godot_svc_module.cpp └── godot_svc_module.h /addons/godot-firebase/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kyle Szklenski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/godot-firebase/README.md: -------------------------------------------------------------------------------- 1 | # Godot Firebase 2 | 3 | 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). 4 | -------------------------------------------------------------------------------- /addons/godot-firebase/Utilities.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name Utilities 3 | 4 | static func get_json_data(value): 5 | if value is PackedByteArray: 6 | value = value.get_string_from_utf8() 7 | var json = JSON.new() 8 | var json_parse_result = json.parse(value) 9 | if json_parse_result == OK: 10 | return json.data 11 | 12 | return null 13 | 14 | 15 | # Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields 16 | # Field Path3D using the "dot" (`.`) notation are supported: 17 | # ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } } 18 | static func dict2fields(dict : Dictionary) -> Dictionary: 19 | var fields = {} 20 | var var_type : String = "" 21 | for field in dict.keys(): 22 | var field_value = dict[field] 23 | if field is String and "." in field: 24 | var keys: Array = field.split(".") 25 | field = keys.pop_front() 26 | keys.reverse() 27 | for key in keys: 28 | field_value = { key : field_value } 29 | 30 | match typeof(field_value): 31 | TYPE_NIL: var_type = "nullValue" 32 | TYPE_BOOL: var_type = "booleanValue" 33 | TYPE_INT: var_type = "integerValue" 34 | TYPE_FLOAT: var_type = "doubleValue" 35 | TYPE_STRING: var_type = "stringValue" 36 | TYPE_DICTIONARY: 37 | if is_field_timestamp(field_value): 38 | var_type = "timestampValue" 39 | field_value = dict2timestamp(field_value) 40 | else: 41 | var_type = "mapValue" 42 | field_value = dict2fields(field_value) 43 | TYPE_ARRAY: 44 | var_type = "arrayValue" 45 | field_value = {"values": array2fields(field_value)} 46 | 47 | if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"): 48 | for key in field_value["fields"].keys(): 49 | fields[field]["mapValue"]["fields"][key] = field_value["fields"][key] 50 | else: 51 | fields[field] = { var_type : field_value } 52 | 53 | return {'fields' : fields} 54 | 55 | 56 | class FirebaseTypeConverter extends RefCounted: 57 | var converters = { 58 | "nullValue": _to_null, 59 | "booleanValue": _to_bool, 60 | "integerValue": _to_int, 61 | "doubleValue": _to_float, 62 | "vector2Value": _to_vector2, 63 | "vector2iValue": _to_vector2i 64 | } 65 | 66 | func convert_value(type, value): 67 | if converters.has(type): 68 | return converters[type].call(value) 69 | return value 70 | 71 | func _to_null(value): 72 | return null 73 | 74 | func _to_bool(value): 75 | return bool(value) 76 | 77 | func _to_int(value): 78 | return int(value) 79 | 80 | func _to_float(value): 81 | return float(value) 82 | 83 | func _to_vector2(value): 84 | return str_to_var(value) as Vector2 85 | 86 | func _to_vector2i(value): 87 | return str_to_var(value) as Vector2i 88 | 89 | static func from_firebase_type(value): 90 | if value == null: 91 | return null 92 | 93 | if value.has("mapValue"): 94 | value = fields2dict(value.values()[0]) 95 | elif value.has("arrayValue"): 96 | value = fields2array(value.values()[0]) 97 | elif value.has("timestampValue"): 98 | value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false) 99 | else: 100 | var converter = FirebaseTypeConverter.new() 101 | var type: String = value.keys()[0] 102 | value = value.values()[0] 103 | if type == "stringValue": 104 | var split_type: String = value.split("(")[0] 105 | if split_type in [ "Vector2", "Vector2i" ]: 106 | type = "{0}Value".format([split_type.to_lower()]) 107 | value = converter.convert_value(type, value) 108 | 109 | return value 110 | 111 | 112 | static func to_firebase_type(value : Variant) -> Dictionary: 113 | var var_type : String = "" 114 | 115 | match typeof(value): 116 | TYPE_NIL: var_type = "nullValue" 117 | TYPE_BOOL: var_type = "booleanValue" 118 | TYPE_INT: var_type = "integerValue" 119 | TYPE_FLOAT: var_type = "doubleValue" 120 | TYPE_STRING: var_type = "stringValue" 121 | TYPE_DICTIONARY: 122 | if is_field_timestamp(value): 123 | var_type = "timestampValue" 124 | value = dict2timestamp(value) 125 | else: 126 | var_type = "mapValue" 127 | value = dict2fields(value) 128 | TYPE_ARRAY: 129 | var_type = "arrayValue" 130 | value = {"values": array2fields(value)} 131 | TYPE_VECTOR2, TYPE_VECTOR2I: 132 | var_type = "stringValue" 133 | value = var_to_str(value) 134 | 135 | return { var_type : value } 136 | 137 | # Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' } 138 | static func fields2dict(doc) -> Dictionary: 139 | var dict = {} 140 | if doc.has("fields"): 141 | var fields = doc["fields"] 142 | 143 | for field in fields.keys(): 144 | if fields[field].has("mapValue"): 145 | dict[field] = (fields2dict(fields[field].mapValue)) 146 | elif fields[field].has("timestampValue"): 147 | dict[field] = timestamp2dict(fields[field].timestampValue) 148 | elif fields[field].has("arrayValue"): 149 | dict[field] = fields2array(fields[field].arrayValue) 150 | elif fields[field].has("integerValue"): 151 | dict[field] = fields[field].values()[0] as int 152 | elif fields[field].has("doubleValue"): 153 | dict[field] = fields[field].values()[0] as float 154 | elif fields[field].has("booleanValue"): 155 | dict[field] = fields[field].values()[0] as bool 156 | elif fields[field].has("nullValue"): 157 | dict[field] = null 158 | else: 159 | dict[field] = fields[field].values()[0] 160 | return dict 161 | 162 | # Pass an Array to parse it to a Firebase arrayValue 163 | static func array2fields(array : Array) -> Array: 164 | var fields : Array = [] 165 | var var_type : String = "" 166 | for field in array: 167 | match typeof(field): 168 | TYPE_DICTIONARY: 169 | if is_field_timestamp(field): 170 | var_type = "timestampValue" 171 | field = dict2timestamp(field) 172 | else: 173 | var_type = "mapValue" 174 | field = dict2fields(field) 175 | TYPE_NIL: var_type = "nullValue" 176 | TYPE_BOOL: var_type = "booleanValue" 177 | TYPE_INT: var_type = "integerValue" 178 | TYPE_FLOAT: var_type = "doubleValue" 179 | TYPE_STRING: var_type = "stringValue" 180 | TYPE_ARRAY: var_type = "arrayValue" 181 | _: var_type = "FieldTransform" 182 | fields.append({ var_type : field }) 183 | return fields 184 | 185 | # Pass a Firebase arrayValue Dictionary to convert it back to an Array 186 | static func fields2array(array : Dictionary) -> Array: 187 | var fields : Array = [] 188 | if array.has("values"): 189 | for field in array.values: 190 | var item 191 | match field.keys()[0]: 192 | "mapValue": 193 | item = fields2dict(field.mapValue) 194 | "arrayValue": 195 | item = fields2array(field.arrayValue) 196 | "integerValue": 197 | item = field.values()[0] as int 198 | "doubleValue": 199 | item = field.values()[0] as float 200 | "booleanValue": 201 | item = field.values()[0] as bool 202 | "timestampValue": 203 | item = timestamp2dict(field.timestampValue) 204 | "nullValue": 205 | item = null 206 | _: 207 | item = field.values()[0] 208 | fields.append(item) 209 | return fields 210 | 211 | # Converts a gdscript Dictionary (most likely obtained with Time.get_datetime_dict_from_system()) to a Firebase Timestamp 212 | static func dict2timestamp(dict : Dictionary) -> String: 213 | #dict.erase('weekday') 214 | #dict.erase('dst') 215 | #var dict_values : Array = dict.values() 216 | var time = Time.get_datetime_string_from_datetime_dict(dict, false) 217 | return time 218 | #return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values 219 | 220 | # Converts a Firebase Timestamp back to a gdscript Dictionary 221 | static func timestamp2dict(timestamp : String) -> Dictionary: 222 | return Time.get_datetime_dict_from_datetime_string(timestamp, false) 223 | #var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0} 224 | #var dict : PackedStringArray = timestamp.split("T")[0].split("-") 225 | #dict.append_array(timestamp.split("T")[1].split(":")) 226 | #for value in dict.size(): 227 | #datetime[datetime.keys()[value]] = int(dict[value]) 228 | #return datetime 229 | 230 | static func is_field_timestamp(field : Dictionary) -> bool: 231 | return field.has_all(['year','month','day','hour','minute','second']) 232 | 233 | 234 | # HTTPRequeust seems to have an issue in Web exports where the body returns empty 235 | # This appears to be caused by the gzip compression being unsupported, so we 236 | # disable it when web export is detected. 237 | static func fix_http_request(http_request): 238 | if is_web(): 239 | http_request.accept_gzip = false 240 | 241 | static func is_web() -> bool: 242 | return OS.get_name() in ["HTML5", "Web"] 243 | 244 | 245 | class MultiSignal extends RefCounted: 246 | signal completed(with_signal) 247 | signal all_completed() 248 | 249 | var _has_signaled := false 250 | var _early_exit := false 251 | 252 | var signal_count := 0 253 | 254 | func _init(sigs : Array[Signal], early_exit := true, should_oneshot := true) -> void: 255 | _early_exit = early_exit 256 | for sig in sigs: 257 | add_signal(sig, should_oneshot) 258 | 259 | func add_signal(sig : Signal, should_oneshot) -> void: 260 | signal_count += 1 261 | sig.connect( 262 | func(): 263 | if not _has_signaled and _early_exit: 264 | completed.emit(sig) 265 | _has_signaled = true 266 | elif not _early_exit: 267 | completed.emit(sig) 268 | signal_count -= 1 269 | if signal_count <= 0: # Not sure how it could be less than 270 | all_completed.emit() 271 | , CONNECT_ONE_SHOT if should_oneshot else CONNECT_REFERENCE_COUNTED 272 | ) 273 | 274 | class SignalReducer extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object. 275 | signal completed 276 | 277 | var awaiters : Array[Signal] = [] 278 | 279 | var reducers = { 280 | 0 : func(): completed.emit(), 281 | 1 : func(p): completed.emit(), 282 | 2 : func(p1, p2): completed.emit(), 283 | 3 : func(p1, p2, p3): completed.emit(), 284 | 4 : func(p1, p2, p3, p4): completed.emit() 285 | } 286 | 287 | func add_signal(sig : Signal, param_count : int = 0) -> void: 288 | assert(param_count < 5, "Too many parameters to reduce, just add more!") 289 | sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing 290 | 291 | class SignalReducerWithResult extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object. 292 | signal completed(result) 293 | 294 | var awaiters : Array[Signal] = [] 295 | 296 | var reducers = { 297 | 0 : func(): completed.emit(), 298 | 1 : func(p): completed.emit({1 : p}), 299 | 2 : func(p1, p2): completed.emit({ 1 : p1, 2 : p2 }), 300 | 3 : func(p1, p2, p3): completed.emit({ 1 : p1, 2 : p2, 3 : p3 }), 301 | 4 : func(p1, p2, p3, p4): completed.emit({ 1 : p1, 2 : p2, 3 : p3, 4 : p4 }) 302 | } 303 | 304 | func add_signal(sig : Signal, param_count : int = 0) -> void: 305 | assert(param_count < 5, "Too many parameters to reduce, just add more!") 306 | sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing 307 | 308 | class ObservableDictionary extends RefCounted: 309 | signal keys_changed() 310 | 311 | var _internal : Dictionary 312 | var is_notifying := true 313 | 314 | func _init(copy : Dictionary = {}) -> void: 315 | _internal = copy 316 | 317 | func add(key : Variant, value : Variant) -> void: 318 | _internal[key] = value 319 | if is_notifying: 320 | keys_changed.emit() 321 | 322 | func update(key : Variant, value : Variant) -> void: 323 | _internal[key] = value 324 | if is_notifying: 325 | keys_changed.emit() 326 | 327 | func has(key : Variant) -> bool: 328 | return _internal.has(key) 329 | 330 | func keys(): 331 | return _internal.keys() 332 | 333 | func values(): 334 | return _internal.values() 335 | 336 | func erase(key : Variant) -> bool: 337 | var result = _internal.erase(key) 338 | if is_notifying: 339 | keys_changed.emit() 340 | 341 | return result 342 | 343 | func get_value(key : Variant) -> Variant: 344 | return _internal[key] 345 | 346 | func _get(property: StringName) -> Variant: 347 | if _internal.has(property): 348 | return _internal[property] 349 | 350 | return false 351 | 352 | func _set(property: StringName, value: Variant) -> bool: 353 | update(property, value) 354 | return true 355 | 356 | class AwaitDetachable extends Node2D: 357 | var awaiter : Signal 358 | 359 | func _init(freeable_node, await_signal : Signal) -> void: 360 | awaiter = await_signal 361 | add_child(freeable_node) 362 | awaiter.connect(queue_free) -------------------------------------------------------------------------------- /addons/godot-firebase/auth/auth.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors TODO 2 | ## @meta-version 2.5 3 | ## The authentication API for Firebase. 4 | ## Documentation TODO. 5 | @tool 6 | class_name FirebaseAuth 7 | extends HTTPRequest 8 | 9 | const _API_VERSION : String = "v1" 10 | const _INAPP_PLUGIN : String = "GodotSvc" 11 | 12 | # Emitted for each Auth request issued. 13 | # `result_code` -> Either `1` if auth succeeded or `error_code` if unsuccessful auth request 14 | # `result_content` -> Either `auth_result` if auth succeeded or `error_message` if unsuccessful auth request 15 | signal auth_request(result_code, result_content) 16 | 17 | signal signup_succeeded(auth_result) 18 | signal login_succeeded(auth_result) 19 | signal login_failed(code, message) 20 | signal signup_failed(code, message) 21 | signal userdata_received(userdata) 22 | signal token_exchanged(successful) 23 | signal token_refresh_succeeded(auth_result) 24 | signal logged_out() 25 | 26 | const RESPONSE_SIGNUP : String = "identitytoolkit#SignupNewUserResponse" 27 | const RESPONSE_SIGNIN : String = "identitytoolkit#VerifyPasswordResponse" 28 | const RESPONSE_ASSERTION : String = "identitytoolkit#VerifyAssertionResponse" 29 | const RESPONSE_USERDATA : String = "identitytoolkit#GetAccountInfoResponse" 30 | const RESPONSE_CUSTOM_TOKEN : String = "identitytoolkit#VerifyCustomTokenResponse" 31 | 32 | var _base_url : String = "" 33 | var _refresh_request_base_url = "" 34 | var _signup_request_url : String = "accounts:signUp?key=%s" 35 | var _signin_with_oauth_request_url : String = "accounts:signInWithIdp?key=%s" 36 | var _signin_request_url : String = "accounts:signInWithPassword?key=%s" 37 | var _signin_custom_token_url : String = "accounts:signInWithCustomToken?key=%s" 38 | var _userdata_request_url : String = "accounts:lookup?key=%s" 39 | var _oobcode_request_url : String = "accounts:sendOobCode?key=%s" 40 | var _delete_account_request_url : String = "accounts:delete?key=%s" 41 | var _update_account_request_url : String = "accounts:update?key=%s" 42 | 43 | var _refresh_request_url : String = "/v1/token?key=%s" 44 | var _google_auth_request_url : String = "https://accounts.google.com/o/oauth2/v2/auth?" 45 | 46 | var _config : Dictionary = {} 47 | var auth : Dictionary = {} 48 | var _needs_refresh : bool = false 49 | var is_busy : bool = false 50 | var has_child : bool = false 51 | var is_oauth_login: bool = false 52 | 53 | 54 | var tcp_server : TCPServer = TCPServer.new() 55 | var tcp_timer : Timer = Timer.new() 56 | var tcp_timeout : float = 0.5 57 | 58 | var _headers : PackedStringArray = [ 59 | "Content-Type: application/json", 60 | "Accept: application/json", 61 | ] 62 | 63 | var requesting : int = -1 64 | 65 | enum Requests { 66 | NONE = -1, 67 | EXCHANGE_TOKEN, 68 | LOGIN_WITH_OAUTH 69 | } 70 | 71 | var auth_request_type : int = -1 72 | 73 | enum Auth_Type { 74 | NONE = -1, 75 | LOGIN_EP, 76 | LOGIN_ANON, 77 | LOGIN_CT, 78 | LOGIN_OAUTH, 79 | SIGNUP_EP 80 | } 81 | 82 | var _login_request_body : Dictionary = { 83 | "email":"", 84 | "password":"", 85 | "returnSecureToken": true, 86 | } 87 | 88 | var _oauth_login_request_body : Dictionary = { 89 | "postBody":"", 90 | "requestUri":"", 91 | "returnIdpCredential":false, 92 | "returnSecureToken":true 93 | } 94 | 95 | var _anonymous_login_request_body : Dictionary = { 96 | "returnSecureToken":true 97 | } 98 | 99 | var _refresh_request_body : Dictionary = { 100 | "grant_type":"refresh_token", 101 | "refresh_token":"", 102 | } 103 | 104 | var _custom_token_body : Dictionary = { 105 | "token":"", 106 | "returnSecureToken":true 107 | } 108 | 109 | var _password_reset_body : Dictionary = { 110 | "requestType":"password_reset", 111 | "email":"", 112 | } 113 | 114 | 115 | var _change_email_body : Dictionary = { 116 | "idToken":"", 117 | "email":"", 118 | "returnSecureToken": true, 119 | } 120 | 121 | 122 | var _change_password_body : Dictionary = { 123 | "idToken":"", 124 | "password":"", 125 | "returnSecureToken": true, 126 | } 127 | 128 | 129 | var _account_verification_body : Dictionary = { 130 | "requestType":"verify_email", 131 | "idToken":"", 132 | } 133 | 134 | 135 | var _update_profile_body : Dictionary = { 136 | "idToken":"", 137 | "displayName":"", 138 | "photoUrl":"", 139 | "deleteAttribute":"", 140 | "returnSecureToken":true 141 | } 142 | 143 | var link_account_body : Dictionary = { 144 | "idToken":"", 145 | "email":"", 146 | "password":"", 147 | "returnSecureToken":true 148 | } 149 | 150 | var _local_port : int = 8060 151 | var _local_uri : String = "http://localhost:%s/"%_local_port 152 | var _local_provider : AuthProvider = AuthProvider.new() 153 | 154 | func _ready() -> void: 155 | tcp_timer.wait_time = tcp_timeout 156 | tcp_timer.timeout.connect(_tcp_stream_timer) 157 | 158 | Utilities.fix_http_request(self) 159 | if Utilities.is_web(): 160 | _local_uri += "tmp_js_export.html" 161 | 162 | 163 | # Sets the configuration needed for the plugin to talk to Firebase 164 | # These settings come from the Firebase.gd script automatically 165 | func _set_config(config_json : Dictionary) -> void: 166 | _config = config_json 167 | _signup_request_url %= _config.apiKey 168 | _signin_request_url %= _config.apiKey 169 | _signin_custom_token_url %= _config.apiKey 170 | _signin_with_oauth_request_url %= _config.apiKey 171 | _userdata_request_url %= _config.apiKey 172 | _refresh_request_url %= _config.apiKey 173 | _oobcode_request_url %= _config.apiKey 174 | _delete_account_request_url %= _config.apiKey 175 | _update_account_request_url %= _config.apiKey 176 | 177 | request_completed.connect(_on_FirebaseAuth_request_completed) 178 | _check_emulating() 179 | 180 | 181 | func _check_emulating() -> void : 182 | ## Check emulating 183 | if not Firebase.emulating: 184 | _base_url = "https://identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION }) 185 | _refresh_request_base_url = "https://securetoken.googleapis.com" 186 | else: 187 | var port : String = _config.emulators.ports.authentication 188 | if port == "": 189 | Firebase._printerr("You are in 'emulated' mode, but the port for Authentication has not been configured.") 190 | else: 191 | _base_url = "http://localhost:{port}/identitytoolkit.googleapis.com/{version}/".format({ version = _API_VERSION ,port = port }) 192 | _refresh_request_base_url = "http://localhost:{port}/securetoken.googleapis.com".format({port = port}) 193 | 194 | 195 | # Function is used to check if the auth script is ready to process a request. Returns true if it is not currently processing 196 | # If false it will print an error 197 | func _is_ready() -> bool: 198 | if is_busy: 199 | Firebase._printerr("Firebase Auth is currently busy and cannot process this request") 200 | return false 201 | else: 202 | return true 203 | 204 | # Function cleans the URI and replaces spaces with %20 205 | # As of right now we only replace spaces 206 | # We may need to decide to use the uri_encode() String function 207 | func _clean_url(_url): 208 | _url = _url.replace(' ','%20') 209 | return _url 210 | 211 | # Synchronous call to check if any user is already logged in. 212 | func is_logged_in() -> bool: 213 | return auth != null and auth.has("idtoken") 214 | 215 | 216 | # Called with Firebase.Auth.signup_with_email_and_password(email, password) 217 | # You must pass in the email and password to this function for it to work correctly 218 | func signup_with_email_and_password(email : String, password : String) -> void: 219 | if _is_ready(): 220 | is_busy = true 221 | _login_request_body.email = email 222 | _login_request_body.password = password 223 | auth_request_type = Auth_Type.SIGNUP_EP 224 | var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) 225 | _login_request_body.email = "" 226 | _login_request_body.password = "" 227 | if err != OK: 228 | is_busy = false 229 | Firebase._printerr("Error signing up with password and email: %s" % err) 230 | 231 | 232 | # Called with Firebase.Auth.anonymous_login() 233 | # A successful request is indicated by a 200 OK HTTP status code. 234 | # The response contains the Firebase ID token and refresh token associated with the anonymous user. 235 | # The 'mail' field will be empty since no email is linked to an anonymous user 236 | func login_anonymous() -> void: 237 | if _is_ready(): 238 | is_busy = true 239 | auth_request_type = Auth_Type.LOGIN_ANON 240 | var err = request(_base_url + _signup_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_anonymous_login_request_body)) 241 | if err != OK: 242 | is_busy = false 243 | Firebase._printerr("Error logging in as anonymous: %s" % err) 244 | 245 | # Called with Firebase.Auth.login_with_email_and_password(email, password) 246 | # You must pass in the email and password to this function for it to work correctly 247 | # If the login fails it will return an error code through the function _on_FirebaseAuth_request_completed 248 | func login_with_email_and_password(email : String, password : String) -> void: 249 | if _is_ready(): 250 | is_busy = true 251 | _login_request_body.email = email 252 | _login_request_body.password = password 253 | auth_request_type = Auth_Type.LOGIN_EP 254 | var err = request(_base_url + _signin_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_login_request_body)) 255 | _login_request_body.email = "" 256 | _login_request_body.password = "" 257 | if err != OK: 258 | is_busy = false 259 | Firebase._printerr("Error logging in with password and email: %s" % err) 260 | 261 | # Login with a custom valid token 262 | # The token needs to be generated using an external service/function 263 | func login_with_custom_token(token : String) -> void: 264 | if _is_ready(): 265 | is_busy = true 266 | _custom_token_body.token = token 267 | auth_request_type = Auth_Type.LOGIN_CT 268 | var err = request(_base_url + _signin_custom_token_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_custom_token_body)) 269 | if err != OK: 270 | is_busy = false 271 | Firebase._printerr("Error logging in with custom token: %s" % err) 272 | 273 | # Open a web page in browser redirecting to Google oAuth2 page for the current project 274 | # Once given user's authorization, a token will be generated. 275 | # NOTE** the generated token will be automatically captured and a login request will be made if the token is correct 276 | func get_auth_localhost(provider: AuthProvider = get_GoogleProvider(), port : int = _local_port): 277 | get_auth_with_redirect(provider) 278 | await get_tree().create_timer(0.5).timeout 279 | if has_child == false: 280 | add_child(tcp_timer) 281 | has_child = true 282 | tcp_timer.start() 283 | tcp_server.listen(port, "*") 284 | 285 | 286 | func get_auth_with_redirect(provider: AuthProvider) -> void: 287 | var url_endpoint: String = provider.redirect_uri 288 | for key in provider.params.keys(): 289 | url_endpoint+=key+"="+provider.params[key]+"&" 290 | url_endpoint += provider.params.redirect_type+"="+_local_uri 291 | url_endpoint = _clean_url(url_endpoint) 292 | if Utilities.is_web(): 293 | JavaScriptBridge.eval('window.location.replace("' + url_endpoint + '")') 294 | elif Engine.has_singleton(_INAPP_PLUGIN) and OS.get_name() == "iOS": 295 | #in app for ios if the iOS plugin exists 296 | set_local_provider(provider) 297 | Engine.get_singleton(_INAPP_PLUGIN).popup(url_endpoint) 298 | else: 299 | set_local_provider(provider) 300 | OS.shell_open(url_endpoint) 301 | 302 | 303 | # Login with Google oAuth2. 304 | # A token is automatically obtained using an authorization code using @get_google_auth() 305 | # @provider_id and @request_uri can be changed 306 | func login_with_oauth(_token: String, provider: AuthProvider) -> void: 307 | if _token: 308 | is_oauth_login = true 309 | var token : String = _token.uri_decode() 310 | var is_successful: bool = true 311 | if provider.should_exchange: 312 | exchange_token(token, _local_uri, provider.access_token_uri, provider.get_client_id(), provider.get_client_secret()) 313 | is_successful = await self.token_exchanged 314 | token = auth.accesstoken 315 | if is_successful and _is_ready(): 316 | is_busy = true 317 | _oauth_login_request_body.postBody = "access_token="+token+"&providerId="+provider.provider_id 318 | _oauth_login_request_body.requestUri = _local_uri 319 | requesting = Requests.LOGIN_WITH_OAUTH 320 | auth_request_type = Auth_Type.LOGIN_OAUTH 321 | var err = request(_base_url + _signin_with_oauth_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_oauth_login_request_body)) 322 | _oauth_login_request_body.postBody = "" 323 | _oauth_login_request_body.requestUri = "" 324 | if err != OK: 325 | is_busy = false 326 | Firebase._printerr("Error logging in with oauth: %s" % err) 327 | 328 | # Exchange the authorization oAuth2 code obtained from browser with a proper access id_token 329 | func exchange_token(code : String, redirect_uri : String, request_url: String, _client_id: String, _client_secret: String) -> void: 330 | if _is_ready(): 331 | is_busy = true 332 | var exchange_token_body : Dictionary = { 333 | code = code, 334 | redirect_uri = redirect_uri, 335 | client_id = _client_id, 336 | client_secret = _client_secret, 337 | grant_type = "authorization_code", 338 | } 339 | requesting = Requests.EXCHANGE_TOKEN 340 | var err = request(request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(exchange_token_body)) 341 | if err != OK: 342 | is_busy = false 343 | Firebase._printerr("Error exchanging tokens: %s" % err) 344 | 345 | # Open a web page in browser redirecting to Google oAuth2 page for the current project 346 | # Once given user's authorization, a token will be generated. 347 | # NOTE** with this method, the authorization process will be copy-pasted 348 | func get_google_auth_manual(provider: AuthProvider = _local_provider) -> void: 349 | provider.params.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" 350 | get_auth_with_redirect(provider) 351 | 352 | # A timer used to listen through TCP checked the redirect uri of the request 353 | func _tcp_stream_timer() -> void: 354 | var peer : StreamPeer = tcp_server.take_connection() 355 | if peer != null: 356 | var raw_result : String = peer.get_utf8_string(441) 357 | if raw_result != "" and raw_result.begins_with("GET"): 358 | tcp_timer.stop() 359 | remove_child(tcp_timer) 360 | has_child = false 361 | var token : String = "" 362 | for value in raw_result.split(" ")[1].lstrip("/?").split("&"): 363 | var splitted: PackedStringArray = value.split("=") 364 | if _local_provider.params.response_type in splitted[0]: 365 | token = splitted[1] 366 | break 367 | 368 | if token == "": 369 | login_failed.emit() 370 | peer.disconnect_from_host() 371 | tcp_server.stop() 372 | return 373 | 374 | var data : PackedByteArray = '
🔥 You can close this window now. 🔥
'.to_ascii_buffer() 375 | peer.put_data(("HTTP/1.1 200 OK\n").to_ascii_buffer()) 376 | peer.put_data(("Server: Godot Firebase SDK\n").to_ascii_buffer()) 377 | peer.put_data(("Content-Length: %d\n" % data.size()).to_ascii_buffer()) 378 | peer.put_data("Connection: close\n".to_ascii_buffer()) 379 | peer.put_data(("Content-Type: text/html; charset=UTF-8\n\n").to_ascii_buffer()) 380 | peer.put_data(data) 381 | login_with_oauth(token, _local_provider) 382 | await self.login_succeeded 383 | peer.disconnect_from_host() 384 | tcp_server.stop() 385 | 386 | 387 | # Function used to logout of the system, this will also remove_at the local encrypted auth file if there is one 388 | func logout() -> void: 389 | auth = {} 390 | remove_auth() 391 | logged_out.emit() 392 | 393 | # Checks to see if we need a hard login 394 | func needs_login() -> bool: 395 | var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) 396 | var err = encrypted_file == null 397 | return err 398 | 399 | # Function is called when requesting a manual token refresh 400 | func manual_token_refresh(auth_data): 401 | auth = auth_data 402 | var refresh_token = null 403 | auth = get_clean_keys(auth) 404 | if auth.has("refreshtoken"): 405 | refresh_token = auth.refreshtoken 406 | elif auth.has("refresh_token"): 407 | refresh_token = auth.refresh_token 408 | _needs_refresh = true 409 | _refresh_request_body.refresh_token = refresh_token 410 | var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) 411 | if err != OK: 412 | is_busy = false 413 | Firebase._printerr("Error manually refreshing token: %s" % err) 414 | 415 | 416 | # This function is called whenever there is an authentication request to Firebase 417 | # On an error, this function with emit the signal 'login_failed' and print the error to the console 418 | func _on_FirebaseAuth_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 419 | var json = Utilities.get_json_data(body.get_string_from_utf8()) 420 | is_busy = false 421 | var res 422 | if response_code == 0: 423 | # Mocked error results to trigger the correct signal. 424 | # Can occur if there is no internet connection, or the service is down, 425 | # in which case there is no json_body (and thus parsing would fail). 426 | res = {"error": { 427 | "code": "Connection error", 428 | "message": "Error connecting to auth service"}} 429 | else: 430 | if json == null: 431 | Firebase._printerr("Error while parsing auth body json") 432 | auth_request.emit(ERR_PARSE_ERROR, "Error while parsing auth body json") 433 | return 434 | 435 | res = json 436 | if response_code == HTTPClient.RESPONSE_OK: 437 | if not res.has("kind"): 438 | auth = get_clean_keys(res) 439 | match requesting: 440 | Requests.EXCHANGE_TOKEN: 441 | token_exchanged.emit(true) 442 | begin_refresh_countdown() 443 | # Refresh token countdown 444 | auth_request.emit(1, auth) 445 | 446 | if _needs_refresh: 447 | _needs_refresh = false 448 | if not is_oauth_login: login_succeeded.emit(auth) 449 | else: 450 | match res.kind: 451 | RESPONSE_SIGNUP: 452 | auth = get_clean_keys(res) 453 | signup_succeeded.emit(auth) 454 | begin_refresh_countdown() 455 | RESPONSE_SIGNIN, RESPONSE_ASSERTION, RESPONSE_CUSTOM_TOKEN: 456 | auth = get_clean_keys(res) 457 | login_succeeded.emit(auth) 458 | begin_refresh_countdown() 459 | RESPONSE_USERDATA: 460 | var userdata = FirebaseUserData.new(res.users[0]) 461 | userdata_received.emit(userdata) 462 | auth_request.emit(1, auth) 463 | else: 464 | # error message would be INVALID_EMAIL, EMAIL_NOT_FOUND, INVALID_PASSWORD, USER_DISABLED or WEAK_PASSWORD 465 | if requesting == Requests.EXCHANGE_TOKEN: 466 | token_exchanged.emit(false) 467 | login_failed.emit(res.error, res.error_description) 468 | auth_request.emit(res.error, res.error_description) 469 | else: 470 | var sig = signup_failed if auth_request_type == Auth_Type.SIGNUP_EP else login_failed 471 | sig.emit(res.error.code, res.error.message) 472 | auth_request.emit(res.error.code, res.error.message) 473 | requesting = Requests.NONE 474 | auth_request_type = Auth_Type.NONE 475 | is_oauth_login = false 476 | 477 | 478 | 479 | # Function used to save the auth data provided by Firebase into an encrypted file 480 | # Note this does not work in HTML5 or UWP 481 | func save_auth(auth : Dictionary) -> bool: 482 | var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.WRITE, _config.apiKey) 483 | var err = encrypted_file == null 484 | if err: 485 | Firebase._printerr("Error Opening File. Error Code: " + str(FileAccess.get_open_error())) 486 | else: 487 | encrypted_file.store_line(JSON.stringify(auth)) 488 | return not err 489 | 490 | 491 | # Function used to load the auth data file that has been stored locally 492 | # Note this does not work in HTML5 or UWP 493 | func load_auth() -> bool: 494 | var encrypted_file = FileAccess.open_encrypted_with_pass("user://user.auth", FileAccess.READ, _config.apiKey) 495 | var err = encrypted_file == null 496 | if err: 497 | Firebase._printerr("Error Opening Firebase Auth File. Error Code: " + str(FileAccess.get_open_error())) 498 | auth_request.emit(err, "Error Opening Firebase Auth File.") 499 | else: 500 | var json = JSON.new() 501 | var json_parse_result = json.parse(encrypted_file.get_line()) 502 | if json_parse_result == OK: 503 | var encrypted_file_data = json.data 504 | manual_token_refresh(encrypted_file_data) 505 | return not err 506 | 507 | # Function used to remove_at the local encrypted auth file 508 | func remove_auth() -> void: 509 | if (FileAccess.file_exists("user://user.auth")): 510 | DirAccess.remove_absolute("user://user.auth") 511 | else: 512 | Firebase._printerr("No encrypted auth file exists") 513 | 514 | 515 | # Function to check if there is an encrypted auth data file 516 | # If there is, the game will load it and refresh the token 517 | func check_auth_file() -> bool: 518 | if (FileAccess.file_exists("user://user.auth")): 519 | # Will ensure "auth_request" emitted 520 | return load_auth() 521 | else: 522 | Firebase._printerr("Encrypted Firebase Auth file does not exist") 523 | auth_request.emit(ERR_DOES_NOT_EXIST, "Encrypted Firebase Auth file does not exist") 524 | return false 525 | 526 | 527 | # Function used to change the email account for the currently logged in user 528 | func change_user_email(email : String) -> void: 529 | if _is_ready(): 530 | is_busy = true 531 | _change_email_body.email = email 532 | _change_email_body.idToken = auth.idtoken 533 | var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_email_body)) 534 | if err != OK: 535 | is_busy = false 536 | Firebase._printerr("Error changing user email: %s" % err) 537 | 538 | 539 | # Function used to change the password for the currently logged in user 540 | func change_user_password(password : String) -> void: 541 | if _is_ready(): 542 | is_busy = true 543 | _change_password_body.password = password 544 | _change_password_body.idToken = auth.idtoken 545 | var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_change_password_body)) 546 | if err != OK: 547 | is_busy = false 548 | Firebase._printerr("Error changing user password: %s" % err) 549 | 550 | 551 | # User Profile handlers 552 | func update_account(idToken : String, displayName : String, photoUrl : String, deleteAttribute : PackedStringArray, returnSecureToken : bool) -> void: 553 | if _is_ready(): 554 | is_busy = true 555 | _update_profile_body.idToken = idToken 556 | _update_profile_body.displayName = displayName 557 | _update_profile_body.photoUrl = photoUrl 558 | _update_profile_body.deleteAttribute = deleteAttribute 559 | _update_profile_body.returnSecureToken = returnSecureToken 560 | var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_update_profile_body)) 561 | if err != OK: 562 | is_busy = false 563 | Firebase._printerr("Error updating account: %s" % err) 564 | 565 | # Link account with Email and Password 566 | func link_account(email : String, password : String) -> void: 567 | if _is_ready(): 568 | is_busy = true 569 | link_account_body.idToken = auth.idtoken 570 | link_account_body.email = email 571 | link_account_body.password = password 572 | var err = request(_base_url + _update_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(link_account_body)) 573 | if err != OK: 574 | is_busy = false 575 | Firebase._printerr("Error updating account: %s" % err) 576 | 577 | 578 | # Function to send a account verification email 579 | func send_account_verification_email() -> void: 580 | if _is_ready(): 581 | is_busy = true 582 | _account_verification_body.idToken = auth.idtoken 583 | var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_account_verification_body)) 584 | if err != OK: 585 | is_busy = false 586 | Firebase._printerr("Error sending account verification email: %s" % err) 587 | 588 | 589 | # Function used to reset the password for a user who has forgotten in. 590 | # This will send the users account an email with a password reset link 591 | func send_password_reset_email(email : String) -> void: 592 | if _is_ready(): 593 | is_busy = true 594 | _password_reset_body.email = email 595 | var err = request(_base_url + _oobcode_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_password_reset_body)) 596 | if err != OK: 597 | is_busy = false 598 | Firebase._printerr("Error sending password reset email: %s" % err) 599 | 600 | 601 | # Function called to get all 602 | func get_user_data() -> void: 603 | if _is_ready(): 604 | is_busy = true 605 | if not is_logged_in(): 606 | print_debug("Not logged in") 607 | is_busy = false 608 | return 609 | 610 | var err = request(_base_url + _userdata_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) 611 | if err != OK: 612 | is_busy = false 613 | Firebase._printerr("Error getting user data: %s" % err) 614 | 615 | 616 | # Function used to delete the account of the currently authenticated user 617 | func delete_user_account() -> void: 618 | if _is_ready(): 619 | is_busy = true 620 | var err = request(_base_url + _delete_account_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify({"idToken":auth.idtoken})) 621 | if err != OK: 622 | is_busy = false 623 | Firebase._printerr("Error deleting user: %s" % err) 624 | else: 625 | remove_auth() 626 | 627 | 628 | # 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. 629 | func begin_refresh_countdown() -> void: 630 | var refresh_token = null 631 | var expires_in = 1000 632 | auth = get_clean_keys(auth) 633 | if auth.has("refreshtoken"): 634 | refresh_token = auth.refreshtoken 635 | expires_in = auth.expiresin 636 | elif auth.has("refresh_token"): 637 | refresh_token = auth.refresh_token 638 | expires_in = auth.expires_in 639 | if auth.has("userid"): 640 | auth["localid"] = auth.userid 641 | _needs_refresh = true 642 | token_refresh_succeeded.emit(auth) 643 | await get_tree().create_timer(float(expires_in)).timeout 644 | _refresh_request_body.refresh_token = refresh_token 645 | var err = request(_refresh_request_base_url + _refresh_request_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_refresh_request_body)) 646 | if err != OK: 647 | is_busy = false 648 | Firebase._printerr("Error refreshing via countdown: %s" % err) 649 | 650 | 651 | func get_token_from_url(provider: AuthProvider): 652 | var token_type: String = provider.params.response_type if provider.params.response_type == "code" else "access_token" 653 | if OS.has_feature('web'): 654 | var token = JavaScriptBridge.eval(""" 655 | var url_string = window.location.href.replaceAll('?#', '?'); 656 | var url = new URL(url_string); 657 | url.searchParams.get('"""+token_type+"""'); 658 | """) 659 | JavaScriptBridge.eval("""window.history.pushState({}, null, location.href.split('?')[0]);""") 660 | return token 661 | return null 662 | 663 | 664 | func set_redirect_uri(redirect_uri : String) -> void: 665 | self._local_uri = redirect_uri 666 | 667 | func set_local_provider(provider : AuthProvider) -> void: 668 | self._local_provider = provider 669 | 670 | # This function is used to make all keys lowercase 671 | # This is only used to cut down checked processing errors from Firebase 672 | # This is due to Google have inconsistencies in the API that we are trying to fix 673 | func get_clean_keys(auth_result : Dictionary) -> Dictionary: 674 | var cleaned = {} 675 | for key in auth_result.keys(): 676 | cleaned[key.replace("_", "").to_lower()] = auth_result[key] 677 | return cleaned 678 | 679 | # -------------------- 680 | # PROVIDERS 681 | # -------------------- 682 | 683 | func get_GoogleProvider() -> GoogleProvider: 684 | return GoogleProvider.new(_config.clientId, _config.clientSecret) 685 | 686 | func get_FacebookProvider() -> FacebookProvider: 687 | return FacebookProvider.new(_config.auth_providers.facebook_id, _config.auth_providers.facebook_secret) 688 | 689 | func get_GitHubProvider() -> GitHubProvider: 690 | return GitHubProvider.new(_config.auth_providers.github_id, _config.auth_providers.github_secret) 691 | 692 | func get_TwitterProvider() -> TwitterProvider: 693 | return TwitterProvider.new(_config.auth_providers.twitter_id, _config.auth_providers.twitter_secret) 694 | -------------------------------------------------------------------------------- /addons/godot-firebase/auth/auth_provider.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name AuthProvider 3 | extends RefCounted 4 | 5 | var redirect_uri: String = "" 6 | var access_token_uri: String = "" 7 | var provider_id: String = "" 8 | var params: Dictionary = { 9 | client_id = "", 10 | scope = "", 11 | response_type = "", 12 | state = "", 13 | redirect_type = "redirect_uri", 14 | } 15 | var client_secret: String = "" 16 | var should_exchange: bool = false 17 | 18 | 19 | func set_client_id(client_id: String) -> void: 20 | self.params.client_id = client_id 21 | 22 | func set_client_secret(client_secret: String) -> void: 23 | self.client_secret = client_secret 24 | 25 | func get_client_id() -> String: 26 | return self.params.client_id 27 | 28 | func get_client_secret() -> String: 29 | return self.client_secret 30 | 31 | func get_oauth_params() -> String: 32 | return "" 33 | -------------------------------------------------------------------------------- /addons/godot-firebase/auth/providers/facebook.gd: -------------------------------------------------------------------------------- 1 | class_name FacebookProvider 2 | extends AuthProvider 3 | 4 | func _init(client_id: String,client_secret: String): 5 | randomize() 6 | set_client_id(client_id) 7 | set_client_secret(client_secret) 8 | 9 | self.redirect_uri = "https://www.facebook.com/v13.0/dialog/oauth?" 10 | self.access_token_uri = "https://graph.facebook.com/v13.0/oauth/access_token" 11 | self.provider_id = "facebook.com" 12 | self.params.scope = "public_profile" 13 | self.params.state = str(randf_range(0, 1)) 14 | if Utilities.is_web(): 15 | self.should_exchange = false 16 | self.params.response_type = "token" 17 | else: 18 | self.should_exchange = true 19 | self.params.response_type = "code" 20 | 21 | 22 | -------------------------------------------------------------------------------- /addons/godot-firebase/auth/providers/github.gd: -------------------------------------------------------------------------------- 1 | class_name GitHubProvider 2 | extends AuthProvider 3 | 4 | func _init(client_id: String,client_secret: String): 5 | randomize() 6 | set_client_id(client_id) 7 | set_client_secret(client_secret) 8 | self.should_exchange = true 9 | self.redirect_uri = "https://github.com/login/oauth/authorize?" 10 | self.access_token_uri = "https://github.com/login/oauth/access_token" 11 | self.provider_id = "github.com" 12 | self.params.scope = "user:read" 13 | self.params.state = str(randf_range(0, 1)) 14 | self.params.response_type = "code" 15 | -------------------------------------------------------------------------------- /addons/godot-firebase/auth/providers/google.gd: -------------------------------------------------------------------------------- 1 | class_name GoogleProvider 2 | extends AuthProvider 3 | 4 | func _init(client_id: String,client_secret: String): 5 | set_client_id(client_id) 6 | set_client_secret(client_secret) 7 | self.should_exchange = true 8 | self.redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth?" 9 | self.access_token_uri = "https://oauth2.googleapis.com/token" 10 | self.provider_id = "google.com" 11 | self.params.response_type = "code" 12 | self.params.scope = "email openid profile" 13 | self.params.response_type = "code" 14 | -------------------------------------------------------------------------------- /addons/godot-firebase/auth/providers/twitter.gd: -------------------------------------------------------------------------------- 1 | class_name TwitterProvider 2 | extends AuthProvider 3 | 4 | var request_token_endpoint: String = "https://api.twitter.com/oauth/access_token?oauth_callback=" 5 | 6 | var oauth_header: Dictionary = { 7 | oauth_callback="", 8 | oauth_consumer_key="", 9 | oauth_nonce="", 10 | oauth_signature="", 11 | oauth_signature_method="HMAC-SHA1", 12 | oauth_timestamp="", 13 | oauth_version="1.0" 14 | } 15 | 16 | func _init(client_id: String,client_secret: String): 17 | randomize() 18 | set_client_id(client_id) 19 | set_client_secret(client_secret) 20 | 21 | self.oauth_header.oauth_consumer_key = client_id 22 | self.oauth_header.oauth_nonce = Time.get_ticks_usec() 23 | self.oauth_header.oauth_timestamp = Time.get_ticks_msec() 24 | 25 | 26 | self.should_exchange = true 27 | self.redirect_uri = "https://twitter.com/i/oauth2/authorize?" 28 | self.access_token_uri = "https://api.twitter.com/2/oauth2/token" 29 | self.provider_id = "twitter.com" 30 | self.params.redirect_type = "redirect_uri" 31 | self.params.response_type = "code" 32 | self.params.scope = "users.read" 33 | self.params.state = str(randf_range(0, 1)) 34 | 35 | func get_oauth_params() -> String: 36 | var params: PackedStringArray = [] 37 | for key in self.oauth.keys(): 38 | params.append(key+"="+self.oauth.get(key)) 39 | return "&".join(params) 40 | -------------------------------------------------------------------------------- /addons/godot-firebase/auth/user_data.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors TODO 2 | ## @meta-version 2.3 3 | ## Authentication user data. 4 | ## Documentation TODO. 5 | @tool 6 | class_name FirebaseUserData 7 | extends RefCounted 8 | 9 | var local_id : String = "" # The uid of the current user. 10 | var email : String = "" 11 | var email_verified := false # Whether or not the account's email has been verified. 12 | var password_updated_at : float = 0 # The timestamp, in milliseconds, that the account password was last changed. 13 | var last_login_at : float = 0 # The timestamp, in milliseconds, that the account last logged in at. 14 | var created_at : float = 0 # The timestamp, in milliseconds, that the account was created at. 15 | var provider_user_info : Array = [] 16 | 17 | var provider_id : String = "" 18 | var display_name : String = "" 19 | var photo_url : String = "" 20 | 21 | func _init(p_userdata : Dictionary): 22 | local_id = p_userdata.get("localId", "") 23 | email = p_userdata.get("email", "") 24 | email_verified = p_userdata.get("emailVerified", false) 25 | last_login_at = float(p_userdata.get("lastLoginAt", 0)) 26 | created_at = float(p_userdata.get("createdAt", 0)) 27 | password_updated_at = float(p_userdata.get("passwordUpdatedAt", 0)) 28 | display_name = p_userdata.get("displayName", "") 29 | provider_user_info = p_userdata.get("providerUserInfo", []) 30 | if not provider_user_info.is_empty(): 31 | provider_id = provider_user_info[0].get("providerId", "") 32 | photo_url = provider_user_info[0].get("photoUrl", "") 33 | display_name = provider_user_info[0].get("displayName", "") 34 | 35 | func as_text() -> String: 36 | return _to_string() 37 | 38 | func _to_string() -> String: 39 | var txt = "local_id : %s\n" % local_id 40 | txt += "email : %s\n" % email 41 | txt += "last_login_at : %d\n" % last_login_at 42 | txt += "provider_id : %s\n" % provider_id 43 | txt += "display name : %s\n" % display_name 44 | return txt 45 | -------------------------------------------------------------------------------- /addons/godot-firebase/database/database.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors TODO 2 | ## @meta-version 2.2 3 | ## The Realtime Database API for Firebase. 4 | ## Documentation TODO. 5 | @tool 6 | class_name FirebaseDatabase 7 | extends Node 8 | 9 | var _base_url : String = "" 10 | 11 | var _config : Dictionary = {} 12 | 13 | var _auth : Dictionary = {} 14 | 15 | func _set_config(config_json : Dictionary) -> void: 16 | _config = config_json 17 | _check_emulating() 18 | 19 | func _check_emulating() -> void : 20 | ## Check emulating 21 | if not Firebase.emulating: 22 | _base_url = _config.databaseURL 23 | else: 24 | var port : String = _config.emulators.ports.realtimeDatabase 25 | if port == "": 26 | Firebase._printerr("You are in 'emulated' mode, but the port for Realtime Database has not been configured.") 27 | else: 28 | _base_url = "http://localhost" 29 | 30 | func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: 31 | _auth = auth_result 32 | 33 | func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: 34 | _auth = auth_result 35 | 36 | func _on_FirebaseAuth_logout() -> void: 37 | _auth = {} 38 | 39 | func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference: 40 | var firebase_reference = load("res://addons/godot-firebase/database/firebase_database_reference.tscn").instantiate() 41 | firebase_reference.set_db_path(path, filter) 42 | firebase_reference.set_auth_and_config(_auth, _config) 43 | add_child(firebase_reference) 44 | return firebase_reference 45 | 46 | func get_once_database_reference(path : String, filter : Dictionary = {}) -> FirebaseOnceDatabaseReference: 47 | var firebase_reference = load("res://addons/godot-firebase/database/firebase_once_database_reference.tscn").instantiate() 48 | firebase_reference.set_db_path(path, filter) 49 | firebase_reference.set_auth_and_config(_auth, _config) 50 | add_child(firebase_reference) 51 | return firebase_reference 52 | -------------------------------------------------------------------------------- /addons/godot-firebase/database/database_store.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors TODO 2 | ## @meta-version 2.2 3 | ## Data structure that holds the currently-known data at a given path (a.k.a. reference) in a Firebase Realtime Database. 4 | ## Can process both puts and patches into the data based checked realtime events received from the service. 5 | @tool 6 | class_name FirebaseDatabaseStore 7 | extends Node 8 | 9 | const _DELIMITER : String = "/" 10 | const _ROOT : String = "_root" 11 | 12 | ## @default false 13 | ## Whether the store is in debug mode. 14 | var debug : bool = false 15 | var _data : Dictionary = { } 16 | 17 | 18 | ## @args path, payload 19 | ## Puts a new payload into this data store at the given path. Any existing values in this data store 20 | ## at the specified path will be completely erased. 21 | func put(path : String, payload) -> void: 22 | _update_data(path, payload, false) 23 | 24 | ## @args path, payload 25 | ## Patches an update payload into this data store at the specified path. 26 | ## 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. 27 | func patch(path : String, payload) -> void: 28 | _update_data(path, payload, true) 29 | 30 | ## @args path, payload 31 | ## Deletes data at the reference point provided 32 | ## 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 33 | func delete(path : String, payload) -> void: 34 | _update_data(path, payload, true) 35 | 36 | ## Returns a deep copy of this data store's payload. 37 | func get_data() -> Dictionary: 38 | return _data[_ROOT].duplicate(true) 39 | 40 | # 41 | # Updates this data store by either putting or patching the provided payload into it at the given 42 | # path. The provided payload can technically be any value. 43 | # 44 | func _update_data(path: String, payload, patch: bool) -> void: 45 | if debug: 46 | print("Updating data store (patch = %s) (%s = %s)..." % [patch, path, payload]) 47 | 48 | # 49 | # Remove any leading separators. 50 | # 51 | path = path.lstrip(_DELIMITER) 52 | 53 | # 54 | # Traverse the path. 55 | # 56 | var dict = _data 57 | var keys = PackedStringArray([_ROOT]) 58 | 59 | keys.append_array(path.split(_DELIMITER, false)) 60 | 61 | var final_key_idx = (keys.size() - 1) 62 | var final_key = (keys[final_key_idx]) 63 | 64 | keys.remove_at(final_key_idx) 65 | 66 | for key in keys: 67 | if !dict.has(key): 68 | dict[key] = { } 69 | 70 | dict = dict[key] 71 | 72 | # 73 | # Handle non-patch (a.k.a. put) mode and then update the destination value. 74 | # 75 | var new_type = typeof(payload) 76 | 77 | if !patch: 78 | dict.erase(final_key) 79 | 80 | if new_type == TYPE_NIL: 81 | dict.erase(final_key) 82 | elif new_type == TYPE_DICTIONARY: 83 | if !dict.has(final_key): 84 | dict[final_key] = { } 85 | 86 | _update_dictionary(dict[final_key], payload) 87 | else: 88 | dict[final_key] = payload 89 | 90 | if debug: 91 | print("...Data store updated (%s)." % _data) 92 | 93 | # 94 | # Helper method to "blit" changes in an update dictionary payload onto an original dictionary. 95 | # Parameters are directly changed via reference. 96 | # 97 | func _update_dictionary(original_dict: Dictionary, update_payload: Dictionary) -> void: 98 | for key in update_payload.keys(): 99 | var val_type = typeof(update_payload[key]) 100 | 101 | if val_type == TYPE_NIL: 102 | original_dict.erase(key) 103 | elif val_type == TYPE_DICTIONARY: 104 | if !original_dict.has(key): 105 | original_dict[key] = { } 106 | 107 | _update_dictionary(original_dict[key], update_payload[key]) 108 | else: 109 | original_dict[key] = update_payload[key] 110 | -------------------------------------------------------------------------------- /addons/godot-firebase/database/firebase_database_reference.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://btltp52tywbe4"] 2 | 3 | [ext_resource type="Script" path="res://addons/godot-firebase/database/reference.gd" id="1_l3oy5"] 4 | [ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_0qpk7"] 5 | [ext_resource type="Script" path="res://addons/http-sse-client/HTTPSSEClient.gd" id="2_4l0io"] 6 | [ext_resource type="Script" path="res://addons/godot-firebase/database/database_store.gd" id="3_c3r2w"] 7 | 8 | [node name="FirebaseDatabaseReference" type="Node"] 9 | script = ExtResource("1_l3oy5") 10 | 11 | [node name="Pusher" parent="." instance=ExtResource("2_0qpk7")] 12 | 13 | [node name="Listener" type="Node" parent="."] 14 | script = ExtResource("2_4l0io") 15 | 16 | [node name="DataStore" type="Node" parent="."] 17 | script = ExtResource("3_c3r2w") 18 | -------------------------------------------------------------------------------- /addons/godot-firebase/database/firebase_once_database_reference.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://d1u1bxp2fd60e"] 2 | 3 | [ext_resource type="Script" path="res://addons/godot-firebase/database/once_reference.gd" id="1_hq5s2"] 4 | [ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_t2f32"] 5 | 6 | [node name="FirebaseOnceDatabaseReference" type="Node"] 7 | script = ExtResource("1_hq5s2") 8 | 9 | [node name="Pusher" parent="." instance=ExtResource("2_t2f32")] 10 | accept_gzip = false 11 | 12 | [node name="Oncer" parent="." instance=ExtResource("2_t2f32")] 13 | accept_gzip = false 14 | 15 | [connection signal="queue_request_completed" from="Pusher" to="." method="on_push_request_complete"] 16 | [connection signal="queue_request_completed" from="Oncer" to="." method="on_get_request_complete"] 17 | -------------------------------------------------------------------------------- /addons/godot-firebase/database/once_reference.gd: -------------------------------------------------------------------------------- 1 | class_name FirebaseOnceDatabaseReference 2 | extends Node 3 | 4 | 5 | ## @meta-authors BackAt50Ft 6 | ## @meta-version 1.0 7 | ## A once off reference to a location in the Realtime Database. 8 | ## Documentation TODO. 9 | 10 | signal once_successful(dataSnapshot) 11 | signal once_failed() 12 | 13 | signal push_successful() 14 | signal push_failed() 15 | 16 | const ORDER_BY : String = "orderBy" 17 | const LIMIT_TO_FIRST : String = "limitToFirst" 18 | const LIMIT_TO_LAST : String = "limitToLast" 19 | const START_AT : String = "startAt" 20 | const END_AT : String = "endAt" 21 | const EQUAL_TO : String = "equalTo" 22 | 23 | @onready var _oncer = $Oncer 24 | @onready var _pusher = $Pusher 25 | 26 | var _auth : Dictionary 27 | var _config : Dictionary 28 | var _filter_query : Dictionary 29 | var _db_path : String 30 | 31 | const _separator : String = "/" 32 | const _json_list_tag : String = ".json" 33 | const _query_tag : String = "?" 34 | const _auth_tag : String = "auth=" 35 | 36 | const _auth_variable_begin : String = "[" 37 | const _auth_variable_end : String = "]" 38 | const _filter_tag : String = "&" 39 | const _escaped_quote : String = '"' 40 | const _equal_tag : String = "=" 41 | const _key_filter_tag : String = "$key" 42 | 43 | var _headers : PackedStringArray = [] 44 | 45 | func set_db_path(path : String, filter_query_dict : Dictionary) -> void: 46 | _db_path = path 47 | _filter_query = filter_query_dict 48 | 49 | func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void: 50 | _auth = auth_ref 51 | _config = config_ref 52 | 53 | # 54 | # Gets a data snapshot once at the position passed in 55 | # 56 | func once(reference : String) -> void: 57 | var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path() 58 | _oncer.request(ref_pos, _headers, HTTPClient.METHOD_GET, "") 59 | 60 | func _get_remaining_path(is_push : bool = true) -> String: 61 | var remaining_path = "" 62 | if _filter_query_empty() or is_push: 63 | remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken 64 | else: 65 | remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken 66 | 67 | if Firebase.emulating: 68 | remaining_path += "&ns="+_config.projectId+"-default-rtdb" 69 | 70 | return remaining_path 71 | 72 | func _get_list_url(with_port:bool = true) -> String: 73 | var url = Firebase.Database._base_url.trim_suffix(_separator) 74 | if with_port and Firebase.emulating: 75 | url += ":" + _config.emulators.ports.realtimeDatabase 76 | return url + _separator 77 | 78 | 79 | func _get_filter(): 80 | if _filter_query_empty(): 81 | return "" 82 | 83 | var filter = "" 84 | 85 | if _filter_query.has(ORDER_BY): 86 | filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote 87 | _filter_query.erase(ORDER_BY) 88 | else: 89 | filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... 90 | 91 | for key in _filter_query.keys(): 92 | filter += _filter_tag + key + _equal_tag + _filter_query[key] 93 | 94 | return filter 95 | 96 | func _filter_query_empty() -> bool: 97 | return _filter_query == null or _filter_query.is_empty() 98 | 99 | func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 100 | if response_code == HTTPClient.RESPONSE_OK: 101 | var bod = Utilities.get_json_data(body) 102 | once_successful.emit(bod) 103 | else: 104 | once_failed.emit() 105 | 106 | func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 107 | if response_code == HTTPClient.RESPONSE_OK: 108 | push_successful.emit() 109 | else: 110 | push_failed.emit() 111 | 112 | func push(data : Dictionary) -> void: 113 | var to_push = JSON.stringify(data) 114 | _pusher.request(_get_list_url() + _db_path + _get_remaining_path(true), _headers, HTTPClient.METHOD_POST, to_push) 115 | 116 | func update(path : String, data : Dictionary) -> void: 117 | path = path.strip_edges(true, true) 118 | 119 | if path == _separator: 120 | path = "" 121 | 122 | var to_update = JSON.stringify(data) 123 | var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) 124 | _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) 125 | -------------------------------------------------------------------------------- /addons/godot-firebase/database/reference.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors BackAt50Ft 2 | ## @meta-version 2.4 3 | ## A reference to a location in the Realtime Database. 4 | ## Documentation TODO. 5 | @tool 6 | class_name FirebaseDatabaseReference 7 | extends Node 8 | 9 | signal new_data_update(data) 10 | signal patch_data_update(data) 11 | signal delete_data_update(data) 12 | 13 | signal once_successful(dataSnapshot) 14 | signal once_failed() 15 | 16 | signal push_successful() 17 | signal push_failed() 18 | 19 | 20 | const ORDER_BY : String = "orderBy" 21 | const LIMIT_TO_FIRST : String = "limitToFirst" 22 | const LIMIT_TO_LAST : String = "limitToLast" 23 | const START_AT : String = "startAt" 24 | const END_AT : String = "endAt" 25 | const EQUAL_TO : String = "equalTo" 26 | 27 | @onready var _pusher := $Pusher 28 | @onready var _listener := $Listener 29 | @onready var _store := $DataStore 30 | 31 | var _auth : Dictionary 32 | var _config : Dictionary 33 | var _filter_query : Dictionary 34 | var _db_path : String 35 | var _cached_filter : String 36 | var _can_connect_to_host : bool = false 37 | 38 | const _put_tag : String = "put" 39 | const _patch_tag : String = "patch" 40 | const _delete_tag : String = "delete" 41 | const _separator : String = "/" 42 | const _json_list_tag : String = ".json" 43 | const _query_tag : String = "?" 44 | const _auth_tag : String = "auth=" 45 | const _accept_header : String = "accept: text/event-stream" 46 | const _auth_variable_begin : String = "[" 47 | const _auth_variable_end : String = "]" 48 | const _filter_tag : String = "&" 49 | const _escaped_quote : String = '"' 50 | const _equal_tag : String = "=" 51 | const _key_filter_tag : String = "$key" 52 | 53 | var _headers : PackedStringArray = [] 54 | 55 | func _ready() -> void: 56 | #region Set Listener info 57 | $Listener.new_sse_event.connect(on_new_sse_event) 58 | var base_url = _get_list_url(false).trim_suffix(_separator) 59 | var extended_url = _separator + _db_path + _get_remaining_path(false) 60 | var port = -1 61 | if Firebase.emulating: 62 | port = int(_config.emulators.ports.realtimeDatabase) 63 | $Listener.connect_to_host(base_url, extended_url, port) 64 | #endregion Set Listener info 65 | 66 | #region Set Pusher info 67 | $Pusher.queue_request_completed.connect(on_push_request_complete) 68 | #endregion Set Pusher info 69 | 70 | func set_db_path(path : String, filter_query_dict : Dictionary) -> void: 71 | _db_path = path 72 | _filter_query = filter_query_dict 73 | 74 | func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void: 75 | _auth = auth_ref 76 | _config = config_ref 77 | 78 | func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) -> void: 79 | if data: 80 | var command = event 81 | if command and command != "keep-alive": 82 | _route_data(command, data.path, data.data) 83 | if command == _put_tag: 84 | if data.path == _separator and data.data and data.data.keys().size() > 0: 85 | for key in data.data.keys(): 86 | new_data_update.emit(FirebaseResource.new(_separator + key, data.data[key])) 87 | elif data.path != _separator: 88 | new_data_update.emit(FirebaseResource.new(data.path, data.data)) 89 | elif command == _patch_tag: 90 | patch_data_update.emit(FirebaseResource.new(data.path, data.data)) 91 | elif command == _delete_tag: 92 | delete_data_update.emit(FirebaseResource.new(data.path, data.data)) 93 | 94 | func update(path : String, data : Dictionary) -> void: 95 | path = path.strip_edges(true, true) 96 | 97 | if path == _separator: 98 | path = "" 99 | 100 | var to_update = JSON.stringify(data) 101 | 102 | var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path()) 103 | _pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update) 104 | 105 | func push(data : Dictionary) -> void: 106 | var to_push = JSON.stringify(data) 107 | _pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push) 108 | 109 | func delete(reference : String) -> void: 110 | _pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "") 111 | 112 | # 113 | # Returns a deep copy of the current local copy of the data stored at this reference in the Firebase 114 | # Realtime Database. 115 | # 116 | func get_data() -> Dictionary: 117 | if _store == null: 118 | return { } 119 | 120 | return _store.get_data() 121 | 122 | func _get_remaining_path(is_push : bool = true) -> String: 123 | var remaining_path = "" 124 | if _filter_query_empty() or is_push: 125 | remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken 126 | else: 127 | remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken 128 | 129 | if Firebase.emulating: 130 | remaining_path += "&ns="+_config.projectId+"-default-rtdb" 131 | 132 | return remaining_path 133 | 134 | func _get_list_url(with_port:bool = true) -> String: 135 | var url = Firebase.Database._base_url.trim_suffix(_separator) 136 | if with_port and Firebase.emulating: 137 | url += ":" + _config.emulators.ports.realtimeDatabase 138 | return url + _separator 139 | 140 | 141 | func _get_filter(): 142 | if _filter_query_empty(): 143 | return "" 144 | # At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules. 145 | if _cached_filter != "": 146 | _cached_filter = "" 147 | if _filter_query.has(ORDER_BY): 148 | _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote 149 | _filter_query.erase(ORDER_BY) 150 | else: 151 | _cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all... 152 | for key in _filter_query.keys(): 153 | _cached_filter += _filter_tag + key + _equal_tag + _filter_query[key] 154 | 155 | return _cached_filter 156 | 157 | func _filter_query_empty() -> bool: 158 | return _filter_query == null or _filter_query.is_empty() 159 | 160 | # 161 | # Appropriately updates the current local copy of the data stored at this reference in the Firebase 162 | # Realtime Database. 163 | # 164 | func _route_data(command : String, path : String, data) -> void: 165 | if command == _put_tag: 166 | _store.put(path, data) 167 | elif command == _patch_tag: 168 | _store.patch(path, data) 169 | elif command == _delete_tag: 170 | _store.delete(path, data) 171 | 172 | func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 173 | if response_code == HTTPClient.RESPONSE_OK: 174 | push_successful.emit() 175 | else: 176 | push_failed.emit() 177 | -------------------------------------------------------------------------------- /addons/godot-firebase/database/resource.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors SIsilicon, fenix-hub 2 | ## @meta-version 2.2 3 | ## A generic resource used by Firebase Database. 4 | @tool 5 | class_name FirebaseResource 6 | extends Resource 7 | 8 | var key : String 9 | var data 10 | 11 | func _init(key : String,data): 12 | self.key = key.lstrip("/") 13 | self.data = data 14 | 15 | func _to_string(): 16 | return "{ key:{key}, data:{data} }".format({key = key, data = data}) 17 | -------------------------------------------------------------------------------- /addons/godot-firebase/dynamiclinks/dynamiclinks.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors TODO 2 | ## @meta-authors TODO 3 | ## @meta-version 1.1 4 | ## The dynamic links API for Firebase 5 | ## Documentation TODO. 6 | @tool 7 | class_name FirebaseDynamicLinks 8 | extends Node 9 | 10 | signal dynamic_link_generated(link_result) 11 | signal generate_dynamic_link_error(error) 12 | 13 | const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " 14 | const _API_VERSION : String = "v1" 15 | 16 | var request : int = -1 17 | 18 | var _base_url : String = "" 19 | 20 | var _config : Dictionary = {} 21 | 22 | var _auth : Dictionary 23 | var _request_list_node : HTTPRequest 24 | 25 | var _headers : PackedStringArray = [] 26 | 27 | enum Requests { 28 | NONE = -1, 29 | GENERATE 30 | } 31 | 32 | func _set_config(config_json : Dictionary) -> void: 33 | _config = config_json 34 | _request_list_node = HTTPRequest.new() 35 | Utilities.fix_http_request(_request_list_node) 36 | _request_list_node.request_completed.connect(_on_request_completed) 37 | add_child(_request_list_node) 38 | _check_emulating() 39 | 40 | 41 | func _check_emulating() -> void : 42 | ## Check emulating 43 | if not Firebase.emulating: 44 | _base_url = "https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=%s" 45 | _base_url %= _config.apiKey 46 | else: 47 | var port : String = _config.emulators.ports.dynamicLinks 48 | if port == "": 49 | Firebase._printerr("You are in 'emulated' mode, but the port for Dynamic Links has not been configured.") 50 | else: 51 | _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) 52 | 53 | 54 | var _link_request_body : Dictionary = { 55 | "dynamicLinkInfo": { 56 | "domainUriPrefix": "", 57 | "link": "", 58 | "androidInfo": { 59 | "androidPackageName": "" 60 | }, 61 | "iosInfo": { 62 | "iosBundleId": "" 63 | } 64 | }, 65 | "suffix": { 66 | "option": "" 67 | } 68 | } 69 | 70 | ## @args log_link, APN, IBI, is_unguessable 71 | ## This function is used to generate a dynamic link using the Firebase REST API 72 | ## It will return a JSON with the shortened link 73 | func generate_dynamic_link(long_link : String, APN : String, IBI : String, is_unguessable : bool) -> void: 74 | if not _config.domainUriPrefix or _config.domainUriPrefix == "": 75 | generate_dynamic_link_error.emit("Error: Missing domainUriPrefix in config file. Parameter is required.") 76 | Firebase._printerr("Error: Missing domainUriPrefix in config file. Parameter is required.") 77 | return 78 | 79 | request = Requests.GENERATE 80 | _link_request_body.dynamicLinkInfo.domainUriPrefix = _config.domainUriPrefix 81 | _link_request_body.dynamicLinkInfo.link = long_link 82 | _link_request_body.dynamicLinkInfo.androidInfo.androidPackageName = APN 83 | _link_request_body.dynamicLinkInfo.iosInfo.iosBundleId = IBI 84 | if is_unguessable: 85 | _link_request_body.suffix.option = "UNGUESSABLE" 86 | else: 87 | _link_request_body.suffix.option = "SHORT" 88 | _request_list_node.request(_base_url, _headers, HTTPClient.METHOD_POST, JSON.stringify(_link_request_body)) 89 | 90 | func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 91 | var json = JSON.new() 92 | var json_parse_result = json.parse(body.get_string_from_utf8()) 93 | if json_parse_result == OK: 94 | var result_body = json.data.result # Check this 95 | dynamic_link_generated.emit(result_body.shortLink) 96 | else: 97 | generate_dynamic_link_error.emit(json.get_error_message()) 98 | # This used to return immediately when above, but it should still clear the request, so removing it 99 | 100 | request = Requests.NONE 101 | 102 | func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: 103 | _auth = auth_result 104 | 105 | func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: 106 | _auth = auth_result 107 | 108 | func _on_FirebaseAuth_logout() -> void: 109 | _auth = {} 110 | -------------------------------------------------------------------------------- /addons/godot-firebase/example.env: -------------------------------------------------------------------------------- 1 | [firebase/environment_variables] 2 | 3 | "apiKey"="", 4 | "authDomain"="", 5 | "databaseURL"="", 6 | "projectId"="", 7 | "storageBucket"="", 8 | "messagingSenderId"="", 9 | "appId"="", 10 | "measurementId"="" 11 | "clientId"="" 12 | "clientSecret"="" 13 | "domainUriPrefix"="" 14 | "functionsGeoZone"="" 15 | "cacheLocation"="" 16 | 17 | [firebase/emulators/ports] 18 | 19 | authentication="" 20 | firestore="" 21 | realtimeDatabase="" 22 | functions="" 23 | storage="" 24 | dynamicLinks="" 25 | -------------------------------------------------------------------------------- /addons/godot-firebase/firebase/firebase.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors Kyle Szklenski 2 | ## @meta-version 2.6 3 | ## The Firebase Godot API. 4 | ## 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. 5 | ## - [code]Auth[/code]: Manages user authentication (logging and out, etc...) 6 | ## - [code]Database[/code]: A NonSQL realtime database for managing data in JSON structures. 7 | ## - [code]Firestore[/code]: Similar to Database, but stores data in collections and documents, among other things. 8 | ## - [code]Storage[/code]: Gives access to Cloud Storage; perfect for storing files like images and other assets. 9 | ## - [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. 10 | ## 11 | ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki 12 | @tool 13 | extends Node 14 | 15 | const _ENVIRONMENT_VARIABLES : String = "firebase/environment_variables" 16 | const _EMULATORS_PORTS : String = "firebase/emulators/ports" 17 | const _AUTH_PROVIDERS : String = "firebase/auth_providers" 18 | 19 | ## @type FirebaseAuth 20 | ## The Firebase Authentication API. 21 | @onready var Auth := $Auth 22 | 23 | ## @type FirebaseFirestore 24 | ## The Firebase Firestore API. 25 | @onready var Firestore := $Firestore 26 | 27 | ## @type FirebaseDatabase 28 | ## The Firebase Realtime Database API. 29 | @onready var Database := $Database 30 | 31 | ## @type FirebaseStorage 32 | ## The Firebase Storage API. 33 | @onready var Storage := $Storage 34 | 35 | ## @type FirebaseDynamicLinks 36 | ## The Firebase Dynamic Links API. 37 | @onready var DynamicLinks := $DynamicLinks 38 | 39 | ## @type FirebaseFunctions 40 | ## The Firebase Cloud Functions API 41 | @onready var Functions := $Functions 42 | 43 | ## @type FirebaseRemoteConfig 44 | ## The Firebase Remote Config API 45 | @onready var RemoteConfigAPI := $RemoteConfig 46 | 47 | @export var emulating : bool = false 48 | 49 | # Configuration used by all files in this project 50 | # These values can be found in your Firebase Project 51 | # See the README checked Github for how to access 52 | var _config : Dictionary = { 53 | "apiKey": "", 54 | "authDomain": "", 55 | "databaseURL": "", 56 | "projectId": "", 57 | "storageBucket": "", 58 | "messagingSenderId": "", 59 | "appId": "", 60 | "measurementId": "", 61 | "clientId": "", 62 | "clientSecret" : "", 63 | "domainUriPrefix" : "", 64 | "functionsGeoZone" : "", 65 | "cacheLocation":"", 66 | "emulators": { 67 | "ports" : { 68 | "authentication" : "", 69 | "firestore" : "", 70 | "realtimeDatabase" : "", 71 | "functions" : "", 72 | "storage" : "", 73 | "dynamicLinks" : "" 74 | } 75 | }, 76 | "workarounds":{ 77 | "database_connection_closed_issue": false, # fixes https://github.com/firebase/firebase-tools/issues/3329 78 | }, 79 | "auth_providers": { 80 | "facebook_id":"", 81 | "facebook_secret":"", 82 | "github_id":"", 83 | "github_secret":"", 84 | "twitter_id":"", 85 | "twitter_secret":"" 86 | } 87 | } 88 | 89 | func _ready() -> void: 90 | _load_config() 91 | 92 | 93 | func set_emulated(emulating : bool = true) -> void: 94 | self.emulating = emulating 95 | _check_emulating() 96 | 97 | func _check_emulating() -> void: 98 | if emulating: 99 | print("[Firebase] You are now in 'emulated' mode: the services you are using will try to connect to your local emulators, if available.") 100 | for module in get_children(): 101 | if module.has_method("_check_emulating"): 102 | module._check_emulating() 103 | 104 | func _load_config() -> void: 105 | if not (_config.apiKey != "" and _config.authDomain != ""): 106 | var env = ConfigFile.new() 107 | var err = env.load("res://addons/godot-firebase/.env") 108 | if err == OK: 109 | for key in _config.keys(): 110 | var config_value = _config[key] 111 | if key == "emulators" and config_value.has("ports"): 112 | for port in config_value["ports"].keys(): 113 | config_value["ports"][port] = env.get_value(_EMULATORS_PORTS, port, "") 114 | if key == "auth_providers": 115 | for provider in config_value.keys(): 116 | config_value[provider] = env.get_value(_AUTH_PROVIDERS, provider, "") 117 | else: 118 | var value : String = env.get_value(_ENVIRONMENT_VARIABLES, key, "") 119 | if value == "": 120 | _print("The value for `%s` is not configured. If you are not planning to use it, ignore this message." % key) 121 | else: 122 | _config[key] = value 123 | else: 124 | _printerr("Unable to read .env file at path 'res://addons/godot-firebase/.env'") 125 | 126 | _setup_modules() 127 | 128 | func _setup_modules() -> void: 129 | for module in get_children(): 130 | module._set_config(_config) 131 | if not module.has_method("_on_FirebaseAuth_login_succeeded"): 132 | continue 133 | Auth.login_succeeded.connect(module._on_FirebaseAuth_login_succeeded) 134 | Auth.signup_succeeded.connect(module._on_FirebaseAuth_login_succeeded) 135 | Auth.token_refresh_succeeded.connect(module._on_FirebaseAuth_token_refresh_succeeded) 136 | Auth.logged_out.connect(module._on_FirebaseAuth_logout) 137 | 138 | # ------------- 139 | 140 | func _printerr(error : String) -> void: 141 | printerr("[Firebase Error] >> " + error) 142 | 143 | func _print(msg : String) -> void: 144 | print("[Firebase] >> " + str(msg)) 145 | -------------------------------------------------------------------------------- /addons/godot-firebase/firebase/firebase.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://cvb26atjckwlq"] 2 | 3 | [ext_resource type="Script" path="res://addons/godot-firebase/database/database.gd" id="1"] 4 | [ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore.gd" id="2"] 5 | [ext_resource type="Script" path="res://addons/godot-firebase/firebase/firebase.gd" id="3"] 6 | [ext_resource type="Script" path="res://addons/godot-firebase/auth/auth.gd" id="4"] 7 | [ext_resource type="Script" path="res://addons/godot-firebase/storage/storage.gd" id="5"] 8 | [ext_resource type="Script" path="res://addons/godot-firebase/dynamiclinks/dynamiclinks.gd" id="6"] 9 | [ext_resource type="Script" path="res://addons/godot-firebase/functions/functions.gd" id="7"] 10 | [ext_resource type="PackedScene" uid="uid://5xa6ulbllkjk" path="res://addons/godot-firebase/remote_config/firebase_remote_config.tscn" id="8_mvdf4"] 11 | 12 | [node name="Firebase" type="Node"] 13 | script = ExtResource("3") 14 | 15 | [node name="Auth" type="HTTPRequest" parent="."] 16 | max_redirects = 12 17 | timeout = 10.0 18 | script = ExtResource("4") 19 | 20 | [node name="Firestore" type="Node" parent="."] 21 | script = ExtResource("2") 22 | 23 | [node name="Database" type="Node" parent="."] 24 | script = ExtResource("1") 25 | 26 | [node name="Storage" type="Node" parent="."] 27 | script = ExtResource("5") 28 | 29 | [node name="DynamicLinks" type="Node" parent="."] 30 | script = ExtResource("6") 31 | 32 | [node name="Functions" type="Node" parent="."] 33 | script = ExtResource("7") 34 | 35 | [node name="RemoteConfig" parent="." instance=ExtResource("8_mvdf4")] 36 | accept_gzip = false 37 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/field_transform.gd: -------------------------------------------------------------------------------- 1 | extends FirestoreTransform 2 | class_name FieldTransform 3 | 4 | enum TransformType { SetToServerValue, Maximum, Minimum, Increment, AppendMissingElements, RemoveAllFromArray } 5 | 6 | const transtype_string_map = { 7 | TransformType.SetToServerValue : "setToServerValue", 8 | TransformType.Increment : "increment", 9 | TransformType.Maximum : "maximum", 10 | TransformType.Minimum : "minimum", 11 | TransformType.AppendMissingElements : "appendMissingElements", 12 | TransformType.RemoveAllFromArray : "removeAllFromArray" 13 | } 14 | 15 | var document_exists : bool 16 | var document_name : String 17 | var field_path : String 18 | var transform_type : TransformType 19 | var value : Variant 20 | 21 | func get_transform_type() -> String: 22 | return transtype_string_map[transform_type] 23 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/field_transform_array.gd: -------------------------------------------------------------------------------- 1 | class_name FieldTransformArray 2 | extends RefCounted 3 | 4 | var transforms = [] 5 | 6 | var _extended_url 7 | var _collection_name 8 | const _separator = "/" 9 | 10 | func set_config(config : Dictionary): 11 | _extended_url = config.extended_url 12 | _collection_name = config.collection_name 13 | 14 | func push_back(transform : FieldTransform) -> void: 15 | transforms.push_back(transform) 16 | 17 | func serialize() -> Dictionary: 18 | var body = {} 19 | var writes_array = [] 20 | for transform in transforms: 21 | writes_array.push_back({ 22 | "currentDocument": { "exists" : transform.document_exists }, 23 | "transform" : { 24 | "document": _extended_url + _collection_name + _separator + transform.document_name, 25 | "fieldTransforms": [ 26 | { 27 | "fieldPath": transform.field_path, 28 | transform.get_transform_type(): transform.value 29 | }] 30 | } 31 | }) 32 | 33 | body = { "writes": writes_array } 34 | 35 | return body 36 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/field_transforms/decrement_transform.gd: -------------------------------------------------------------------------------- 1 | class_name DecrementTransform 2 | extends FieldTransform 3 | 4 | func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void: 5 | document_name = doc_name 6 | document_exists = doc_must_exist 7 | field_path = path_to_field 8 | 9 | transform_type = FieldTransform.TransformType.Increment 10 | 11 | var value_type = typeof(by_this_much) 12 | if value_type == TYPE_INT: 13 | self.value = { 14 | "integerValue": -by_this_much 15 | } 16 | elif value_type == TYPE_FLOAT: 17 | self.value = { 18 | "doubleValue": -by_this_much 19 | } 20 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/field_transforms/increment_transform.gd: -------------------------------------------------------------------------------- 1 | class_name IncrementTransform 2 | extends FieldTransform 3 | 4 | func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void: 5 | document_name = doc_name 6 | document_exists = doc_must_exist 7 | field_path = path_to_field 8 | 9 | transform_type = FieldTransform.TransformType.Increment 10 | 11 | var value_type = typeof(by_this_much) 12 | if value_type == TYPE_INT: 13 | self.value = { 14 | "integerValue": by_this_much 15 | } 16 | elif value_type == TYPE_FLOAT: 17 | self.value = { 18 | "doubleValue": by_this_much 19 | } 20 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/field_transforms/max_transform.gd: -------------------------------------------------------------------------------- 1 | class_name MaxTransform 2 | extends FieldTransform 3 | 4 | func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void: 5 | document_name = doc_name 6 | document_exists = doc_must_exist 7 | field_path = path_to_field 8 | 9 | transform_type = FieldTransform.TransformType.Maximum 10 | 11 | var value_type = typeof(value) 12 | if value_type == TYPE_INT: 13 | self.value = { 14 | "integerValue": value 15 | } 16 | elif value_type == TYPE_FLOAT: 17 | self.value = { 18 | "doubleValue": value 19 | } 20 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/field_transforms/min_transform.gd: -------------------------------------------------------------------------------- 1 | class_name MinTransform 2 | extends FieldTransform 3 | 4 | func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void: 5 | document_name = doc_name 6 | document_exists = doc_must_exist 7 | field_path = path_to_field 8 | 9 | transform_type = FieldTransform.TransformType.Minimum 10 | 11 | var value_type = typeof(value) 12 | if value_type == TYPE_INT: 13 | self.value = { 14 | "integerValue": value 15 | } 16 | elif value_type == TYPE_FLOAT: 17 | self.value = { 18 | "doubleValue": value 19 | } 20 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/field_transforms/server_timestamp_transform.gd: -------------------------------------------------------------------------------- 1 | class_name ServerTimestampTransform 2 | extends FieldTransform 3 | 4 | func _init(doc_name : String, doc_must_exist : bool, path_to_field : String) -> void: 5 | document_name = doc_name 6 | document_exists = doc_must_exist 7 | field_path = path_to_field 8 | 9 | transform_type = FieldTransform.TransformType.SetToServerValue 10 | value = "REQUEST_TIME" 11 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/firestore.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors Nicolò 'fenix' Santilio, 2 | ## @meta-version 2.5 3 | ## 4 | ## Referenced by [code]Firebase.Firestore[/code]. Represents the Firestore module. 5 | ## Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud. 6 | ## 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. 7 | ## 8 | ## 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. 9 | ## 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. 10 | ## The Cloud Firestore data model supports whatever data structure works best for your app. 11 | ## 12 | ## (source: [url=https://firebase.google.com/docs/firestore]Firestore[/url]) 13 | ## 14 | ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore 15 | @tool 16 | class_name FirebaseFirestore 17 | extends Node 18 | 19 | const _API_VERSION : String = "v1" 20 | 21 | ## Emitted when a [code]list()[/code] or [code]query()[/code] request is [b]not[/b] successfully completed. 22 | signal error(code, status, message) 23 | 24 | enum Requests { 25 | NONE = -1, ## Firestore is not processing any request. 26 | LIST, ## Firestore is processing a [code]list()[/code] request checked a collection. 27 | QUERY ## Firestore is processing a [code]query()[/code] request checked a collection. 28 | } 29 | 30 | # TODO: Implement cache size limit 31 | const CACHE_SIZE_UNLIMITED = -1 32 | 33 | const _CACHE_EXTENSION : String = ".fscache" 34 | const _CACHE_RECORD_FILE : String = "RmlyZXN0b3JlIGNhY2hlLXJlY29yZHMu.fscache" 35 | 36 | const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " 37 | 38 | const _MAX_POOLED_REQUEST_AGE = 30 39 | 40 | ## The code indicating the request Firestore is processing. 41 | ## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers. 42 | ## @enum Requests 43 | var request: int = -1 44 | 45 | ## A Dictionary containing all authentication fields for the current logged user. 46 | ## @type Dictionary 47 | var auth: Dictionary 48 | 49 | var _config: Dictionary = {} 50 | var _cache_loc: String 51 | var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_unique_id() 52 | 53 | 54 | var _base_url: String = "" 55 | var _extended_url: String = "projects/[PROJECT_ID]/databases/(default)/documents/" 56 | var _query_suffix: String = ":runQuery" 57 | var _agg_query_suffix: String = ":runAggregationQuery" 58 | 59 | #var _connect_check_node : HTTPRequest 60 | 61 | var _request_list_node: HTTPRequest 62 | var _requests_queue: Array = [] 63 | var _current_query: FirestoreQuery 64 | 65 | ## Returns a reference collection by its [i]path[/i]. 66 | ## 67 | ## The returned object will be of [code]FirestoreCollection[/code] type. 68 | ## If saved into a variable, it can be used to issue requests checked the collection itself. 69 | ## @args path 70 | ## @return FirestoreCollection 71 | func collection(path : String) -> FirestoreCollection: 72 | for coll in get_children(): 73 | if coll is FirestoreCollection: 74 | if coll.collection_name == path: 75 | return coll 76 | 77 | var coll : FirestoreCollection = FirestoreCollection.new() 78 | coll._extended_url = _extended_url 79 | coll._base_url = _base_url 80 | coll._config = _config 81 | coll.auth = auth 82 | coll.collection_name = path 83 | add_child(coll) 84 | return coll 85 | 86 | 87 | ## Issue a query checked your Firestore database. 88 | ## 89 | ## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query. 90 | ## When awaited, this function returns the resulting array from the query. 91 | ## 92 | ## ex. 93 | ## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code] 94 | ## 95 | ## [b]Warning:[/b] It currently does not work offline! 96 | ## 97 | ## @args query 98 | ## @arg-types FirestoreQuery 99 | ## @return Array[FirestoreDocument] 100 | func query(query : FirestoreQuery) -> Array: 101 | if query.aggregations.size() > 0: 102 | Firebase._printerr("Aggregation query sent with normal query call: " + str(query)) 103 | return [] 104 | 105 | var task : FirestoreTask = FirestoreTask.new() 106 | task.action = FirestoreTask.Task.TASK_QUERY 107 | var body: Dictionary = { structuredQuery = query.query } 108 | var url: String = _base_url + _extended_url + query.sub_collection_path + _query_suffix 109 | 110 | task.data = query 111 | task._fields = JSON.stringify(body) 112 | task._url = url 113 | _pooled_request(task) 114 | return await _handle_task_finished(task) 115 | 116 | ## Issue an aggregation query (sum, average, count) against your Firestore database; 117 | ## cheaper than a normal query and counting (for instance) values directly. 118 | ## 119 | ## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query. 120 | ## When awaited, this function returns the result from the aggregation query. 121 | ## 122 | ## ex. 123 | ## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code] 124 | ## 125 | ## [b]Warning:[/b] It currently does not work offline! 126 | ## 127 | ## @args query 128 | ## @arg-types FirestoreQuery 129 | ## @return Variant representing the array results of the aggregation query 130 | func aggregation_query(query : FirestoreQuery) -> Variant: 131 | if query.aggregations.size() == 0: 132 | Firebase._printerr("Aggregation query sent with no aggregation values: " + str(query)) 133 | return 0 134 | 135 | var task : FirestoreTask = FirestoreTask.new() 136 | task.action = FirestoreTask.Task.TASK_AGG_QUERY 137 | 138 | var body: Dictionary = { structuredAggregationQuery = { structuredQuery = query.query, aggregations = query.aggregations } } 139 | var url: String = _base_url + _extended_url + _agg_query_suffix 140 | 141 | task.data = query 142 | task._fields = JSON.stringify(body) 143 | task._url = url 144 | _pooled_request(task) 145 | var result = await _handle_task_finished(task) 146 | return result 147 | 148 | ## 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] 149 | ## @args collection_id, page_size, page_token, order_by 150 | ## @arg-types String, int, String, String 151 | ## @arg-defaults , 0, "", "" 152 | ## @return Array[FirestoreDocument] 153 | func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> Array: 154 | var task : FirestoreTask = FirestoreTask.new() 155 | task.action = FirestoreTask.Task.TASK_LIST 156 | var url : String = _base_url + _extended_url + path 157 | if page_size != 0: 158 | url+="?pageSize="+str(page_size) 159 | if page_token != "": 160 | url+="&pageToken="+page_token 161 | if order_by != "": 162 | url+="&orderBy="+order_by 163 | 164 | task.data = [path, page_size, page_token, order_by] 165 | task._url = url 166 | _pooled_request(task) 167 | 168 | return await _handle_task_finished(task) 169 | 170 | 171 | func _set_config(config_json : Dictionary) -> void: 172 | _config = config_json 173 | _cache_loc = _config["cacheLocation"] 174 | _extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId) 175 | 176 | # 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. 177 | 178 | _check_emulating() 179 | 180 | func _check_emulating() -> void : 181 | ## Check emulating 182 | if not Firebase.emulating: 183 | _base_url = "https://firestore.googleapis.com/{version}/".format({ version = _API_VERSION }) 184 | else: 185 | var port : String = _config.emulators.ports.firestore 186 | if port == "": 187 | Firebase._printerr("You are in 'emulated' mode, but the port for Firestore has not been configured.") 188 | else: 189 | _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) 190 | 191 | func _pooled_request(task : FirestoreTask) -> void: 192 | if (auth == null or auth.is_empty()) and not Firebase.emulating: 193 | Firebase._print("Unauthenticated request issued...") 194 | Firebase.Auth.login_anonymous() 195 | var result : Array = await Firebase.Auth.auth_request 196 | if result[0] != 1: 197 | _check_auth_error(result[0], result[1]) 198 | Firebase._print("Client connected as Anonymous") 199 | 200 | if not Firebase.emulating: 201 | task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) 202 | 203 | var http_request = HTTPRequest.new() 204 | http_request.timeout = 5 205 | Utilities.fix_http_request(http_request) 206 | add_child(http_request) 207 | http_request.request_completed.connect( 208 | func(result, response_code, headers, body): 209 | task._on_request_completed(result, response_code, headers, body) 210 | http_request.queue_free() 211 | ) 212 | 213 | http_request.request(task._url, task._headers, task._method, task._fields) 214 | 215 | func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: 216 | auth = auth_result 217 | for coll in get_children(): 218 | if coll is FirestoreCollection: 219 | coll.auth = auth 220 | 221 | func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: 222 | auth = auth_result 223 | for coll in get_children(): 224 | if coll is FirestoreCollection: 225 | coll.auth = auth 226 | 227 | func _on_FirebaseAuth_logout() -> void: 228 | auth = {} 229 | 230 | func _check_auth_error(code : int, message : String) -> void: 231 | var err : String 232 | match code: 233 | 400: err = "Please enable the Anonymous Sign-in method, or Authenticate the Client before issuing a request" 234 | Firebase._printerr(err) 235 | Firebase._printerr(message) 236 | 237 | func _handle_task_finished(task : FirestoreTask): 238 | await task.task_finished 239 | 240 | if task.error.keys().size() > 0: 241 | error.emit(task.error) 242 | 243 | return task.data 244 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/firestore_collection.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors TODO 2 | ## @meta-authors TODO 3 | ## @meta-version 2.3 4 | ## A reference to a Firestore Collection. 5 | ## Documentation TODO. 6 | @tool 7 | class_name FirestoreCollection 8 | extends Node 9 | 10 | signal error(error_result) 11 | 12 | const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " 13 | 14 | const _separator : String = "/" 15 | const _query_tag : String = "?" 16 | const _documentId_tag : String = "documentId=" 17 | 18 | var auth : Dictionary 19 | var collection_name : String 20 | 21 | var _base_url : String 22 | var _extended_url : String 23 | var _config : Dictionary 24 | 25 | # ----------------------- Requests 26 | 27 | ## @args document_id 28 | ## @return FirestoreTask 29 | ## used to GET a document from the collection, specify @document_id 30 | func get_doc(document_id : String, from_cache : bool = false, is_listener : bool = false) -> FirestoreDocument: 31 | if from_cache: 32 | # for now, just return the child directly; in the future, make it smarter so there's a default, if long, polling time for this 33 | for child in get_children(): 34 | if child.doc_name == document_id: 35 | return child 36 | 37 | var task : FirestoreTask = FirestoreTask.new() 38 | task.action = FirestoreTask.Task.TASK_GET 39 | task.data = collection_name + "/" + document_id 40 | var url = _get_request_url() + _separator + document_id.replace(" ", "%20") 41 | 42 | _process_request(task, document_id, url) 43 | var result = await Firebase.Firestore._handle_task_finished(task) 44 | if result != null: 45 | var found_document = false 46 | for child in get_children(): 47 | if child.doc_name == document_id: 48 | child.replace(result, true) 49 | result = child 50 | found_document = true 51 | break 52 | 53 | if not found_document: 54 | add_child(result, true) 55 | else: 56 | print("get_document returned null for %s %s" % [collection_name, document_id]) 57 | 58 | return result 59 | 60 | ## @args document_id, fields 61 | ## @arg-defaults , {} 62 | ## @return FirestoreDocument 63 | ## used to ADD a new document to the collection, specify @documentID and @data 64 | func add(document_id : String, data : Dictionary = {}) -> FirestoreDocument: 65 | var task : FirestoreTask = FirestoreTask.new() 66 | task.action = FirestoreTask.Task.TASK_POST 67 | task.data = collection_name + "/" + document_id 68 | var url = _get_request_url() + _query_tag + _documentId_tag + document_id 69 | 70 | _process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data))) 71 | var result = await Firebase.Firestore._handle_task_finished(task) 72 | if result != null: 73 | for child in get_children(): 74 | if child.doc_name == document_id: 75 | child.free() # Consider throwing an error for this since it shouldn't already exist 76 | break 77 | 78 | result.collection_name = collection_name 79 | add_child(result, true) 80 | return result 81 | 82 | ## @args document 83 | ## @return FirestoreDocument 84 | # used to UPDATE a document, specify the document 85 | func update(document : FirestoreDocument) -> FirestoreDocument: 86 | var task : FirestoreTask = FirestoreTask.new() 87 | task.action = FirestoreTask.Task.TASK_PATCH 88 | task.data = collection_name + "/" + document.doc_name 89 | var url = _get_request_url() + _separator + document.doc_name.replace(" ", "%20") + "?" 90 | for key in document.keys(): 91 | url+="updateMask.fieldPaths={key}&".format({key = key}) 92 | 93 | url = url.rstrip("&") 94 | 95 | for key in document.keys(): 96 | if document.get_value(key) == null: 97 | document._erase(key) 98 | 99 | var temp_transforms 100 | if document._transforms != null: 101 | temp_transforms = document._transforms 102 | document._transforms = FieldTransformArray.new() 103 | 104 | var body = JSON.stringify({"fields": document.document}) 105 | 106 | _process_request(task, document.doc_name, url, body) 107 | var result = await Firebase.Firestore._handle_task_finished(task) 108 | if result != null: 109 | for child in get_children(): 110 | if child.doc_name == result.doc_name: 111 | child.replace(result, true) 112 | break 113 | 114 | if temp_transforms != null: 115 | result._transforms = temp_transforms 116 | 117 | document.field_added_or_updated = false 118 | 119 | return result 120 | 121 | 122 | ## @args document 123 | ## @return Dictionary 124 | # Used to commit changes from transforms, specify the document with the transforms 125 | func commit(document : FirestoreDocument) -> Dictionary: 126 | var task : FirestoreTask = FirestoreTask.new() 127 | task.action = FirestoreTask.Task.TASK_COMMIT 128 | var url = get_database_url("commit") 129 | 130 | document._transforms.set_config( 131 | { 132 | "extended_url": _extended_url, 133 | "collection_name": collection_name 134 | } 135 | ) # Only place we can set this is here, oofness 136 | 137 | var body = document._transforms.serialize() 138 | document.clear_field_transforms() 139 | _process_request(task, document.doc_name, url, JSON.stringify(body)) 140 | 141 | 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 142 | 143 | ## @args document_id 144 | ## @return FirestoreTask 145 | # used to DELETE a document, specify the document 146 | func delete(document : FirestoreDocument) -> bool: 147 | var doc_name = document.doc_name 148 | var task : FirestoreTask = FirestoreTask.new() 149 | task.action = FirestoreTask.Task.TASK_DELETE 150 | task.data = document.collection_name + "/" + doc_name 151 | var url = _get_request_url() + _separator + doc_name.replace(" ", "%20") 152 | _process_request(task, doc_name, url) 153 | var result = await Firebase.Firestore._handle_task_finished(task) 154 | 155 | # Clean up the cache 156 | if result: 157 | for node in get_children(): 158 | if node.doc_name == doc_name: 159 | node.free() # Should be only one 160 | break 161 | 162 | return result 163 | 164 | func _get_request_url() -> String: 165 | return _base_url + _extended_url + collection_name 166 | 167 | func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void: 168 | if auth == null or auth.is_empty(): 169 | Firebase._print("Unauthenticated request issued...") 170 | Firebase.Auth.login_anonymous() 171 | var result : Array = await Firebase.Auth.auth_request 172 | if result[0] != 1: 173 | Firebase.Firestore._check_auth_error(result[0], result[1]) 174 | return 175 | Firebase._print("Client authenticated as Anonymous User.") 176 | 177 | task._url = url 178 | task._fields = fields 179 | task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken]) 180 | Firebase.Firestore._pooled_request(task) 181 | 182 | func get_database_url(append) -> String: 183 | return _base_url + _extended_url.rstrip("/") + ":" + append 184 | 185 | 186 | ## @args document_id: StringName, data: Variant 187 | ## @return void 188 | # used to SET a document, specify the document ID and new data 189 | func set_doc(document_id: StringName, data: Variant) -> void: 190 | var task: FirestoreTask = FirestoreTask.new() 191 | task.action = FirestoreTask.Task.TASK_PATCH 192 | task.data = collection_name + "/" + document_id 193 | var url = _get_request_url() + _separator + document_id.replace(" ", "%20") 194 | 195 | _process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data))) 196 | var result = await Firebase.Firestore._handle_task_finished(task) 197 | 198 | if result != null: 199 | for child in get_children(): 200 | if child.doc_name == document_id: 201 | child.replace(result, true) 202 | break 203 | else: 204 | print("set_document returned null for %s %s" % [collection_name, document_id]) 205 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/firestore_document.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors Kyle Szklenski 2 | ## @meta-version 2.2 3 | ## A reference to a Firestore Document. 4 | ## Documentation TODO. 5 | @tool 6 | class_name FirestoreDocument 7 | extends Node 8 | 9 | # A FirestoreDocument objects that holds all important values for a Firestore Document, 10 | # @doc_name = name of the Firestore Document, which is the request PATH 11 | # @doc_fields = fields held by Firestore Document, in APIs format 12 | # created when requested from a `collection().get()` call 13 | 14 | var document : Dictionary # the Document itself 15 | var doc_name : String # only .name 16 | var create_time : String # createTime 17 | var collection_name : String # Name of the collection to which it belongs 18 | var _transforms : FieldTransformArray # The transforms to apply 19 | var field_added_or_updated : bool # true or false if anything has been added or updated since the last update() 20 | signal changed(changes) 21 | 22 | func _init(doc : Dictionary = {}): 23 | _transforms = FieldTransformArray.new() 24 | 25 | if doc.has("fields"): 26 | document = doc.fields 27 | if doc.has("name"): 28 | doc_name = doc.name 29 | if doc_name.count("/") > 2: 30 | doc_name = (doc_name.split("/") as Array).back() 31 | if doc.has("createTime"): 32 | self.create_time = doc.createTime 33 | 34 | func replace(with : FirestoreDocument, is_listener := false) -> void: 35 | var current = document.duplicate() 36 | document = with.document 37 | 38 | var changes = { 39 | "added": [], "removed": [], "updated": [], "is_listener": is_listener 40 | } 41 | 42 | for key in current.keys(): 43 | if not document.has(key): 44 | changes.removed.push_back({ "key" : key }) 45 | else: 46 | var new_value = Utilities.from_firebase_type(document[key]) 47 | var old_value = Utilities.from_firebase_type(current[key]) 48 | if typeof(new_value) != typeof(old_value) or new_value != old_value: 49 | if old_value == null: 50 | changes.removed.push_back({ "key" : key }) # ?? 51 | else: 52 | changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value }) 53 | 54 | for key in document.keys(): 55 | if not current.has(key): 56 | changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) }) 57 | 58 | if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()): 59 | changed.emit(changes) 60 | 61 | func new_document(base_document: Dictionary) -> void: 62 | var current = document.duplicate() 63 | document = {} 64 | for key in base_document.keys(): 65 | document[key] = Utilities.to_firebase_type(key) 66 | 67 | var changes = { 68 | "added": [], "removed": [], "updated": [], "is_listener": false 69 | } 70 | 71 | for key in current.keys(): 72 | if not document.has(key): 73 | changes.removed.push_back({ "key" : key }) 74 | else: 75 | var new_value = Utilities.from_firebase_type(document[key]) 76 | var old_value = Utilities.from_firebase_type(current[key]) 77 | if typeof(new_value) != typeof(old_value) or new_value != old_value: 78 | if old_value == null: 79 | changes.removed.push_back({ "key" : key }) # ?? 80 | else: 81 | changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value }) 82 | 83 | for key in document.keys(): 84 | if not current.has(key): 85 | changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) }) 86 | 87 | if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()): 88 | changed.emit(changes) 89 | 90 | func is_null_value(key) -> bool: 91 | return document.has(key) and Utilities.from_firebase_type(document[key]) == null 92 | 93 | # As of right now, we do not track these with track changes; instead, they'll come back when the document updates from the server. 94 | # 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. 95 | func add_field_transform(transform : FieldTransform) -> void: 96 | _transforms.push_back(transform) 97 | 98 | func remove_field_transform(transform : FieldTransform) -> void: 99 | _transforms.erase(transform) 100 | 101 | func clear_field_transforms() -> void: 102 | _transforms.transforms.clear() 103 | 104 | func remove_field(field_path : String) -> void: 105 | if document.has(field_path): 106 | document[field_path] = Utilities.to_firebase_type(null) 107 | 108 | var changes = { 109 | "added": [], "removed": [], "updated": [], "is_listener": false 110 | } 111 | 112 | changes.removed.push_back({ "key" : field_path }) 113 | changed.emit(changes) 114 | 115 | func _erase(field_path : String) -> void: 116 | document.erase(field_path) 117 | 118 | func add_or_update_field(field_path : String, value : Variant) -> void: 119 | var changes = { 120 | "added": [], "removed": [], "updated": [], "is_listener": false 121 | } 122 | 123 | var existing_value = get_value(field_path) 124 | var has_field_path = existing_value != null and not is_null_value(field_path) 125 | 126 | var converted_value = Utilities.to_firebase_type(value) 127 | document[field_path] = converted_value 128 | 129 | if has_field_path: 130 | changes.updated.push_back({ "key" : field_path, "old" : existing_value, "new" : value }) 131 | else: 132 | changes.added.push_back({ "key" : field_path, "new" : value }) 133 | field_added_or_updated = true 134 | changed.emit(changes) 135 | 136 | func on_snapshot(when_called : Callable, poll_time : float = 1.0) -> FirestoreListener.FirestoreListenerConnection: 137 | if get_child_count() >= 1: # Only one listener per 138 | assert(false, "Multiple listeners not allowed for the same document yet") 139 | return 140 | 141 | changed.connect(when_called, CONNECT_REFERENCE_COUNTED) 142 | var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate() 143 | add_child(listener) 144 | listener.initialize_listener(collection_name, doc_name, poll_time) 145 | listener.owner = self 146 | var result = listener.enable_connection() 147 | return result 148 | 149 | func get_value(property : StringName) -> Variant: 150 | if property == "doc_name": 151 | return doc_name 152 | elif property == "collection_name": 153 | return collection_name 154 | elif property == "create_time": 155 | return create_time 156 | 157 | if document.has(property): 158 | var result = Utilities.from_firebase_type(document[property]) 159 | return result 160 | 161 | return null 162 | 163 | func has_changes_pending() -> bool: 164 | return field_added_or_updated 165 | 166 | func _get(property: StringName) -> Variant: 167 | return get_value(property) 168 | 169 | func _set(property: StringName, value: Variant) -> bool: 170 | assert(value != null, "When using the dictionary setter, the value cannot be null; use erase_field instead.") 171 | document[property] = Utilities.to_firebase_type(value) 172 | return true 173 | 174 | func get_unsafe_document() -> Dictionary: 175 | var result = {} 176 | for key in keys(): 177 | result[key] = Utilities.from_firebase_type(document[key]) 178 | 179 | return result 180 | 181 | func keys(): 182 | return document.keys() 183 | 184 | # Call print(document) to return directly this document formatted 185 | func _to_string() -> String: 186 | return ("doc_name: {doc_name}, \ndata: {data}, \ncreate_time: {create_time}\n").format( 187 | {doc_name = self.doc_name, 188 | data = document, 189 | create_time = self.create_time}) -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/firestore_listener.gd: -------------------------------------------------------------------------------- 1 | class_name FirestoreListener 2 | extends Node 3 | 4 | const MinPollTime = 60 * 2 # seconds, so 2 minutes 5 | 6 | var _doc_name : String 7 | var _poll_time : float 8 | var _collection : FirestoreCollection 9 | 10 | var _total_time = 0.0 11 | var _enabled := false 12 | 13 | func initialize_listener(collection_name : String, doc_name : String, poll_time : float) -> void: 14 | _poll_time = max(poll_time, MinPollTime) 15 | _doc_name = doc_name 16 | _collection = Firebase.Firestore.collection(collection_name) 17 | 18 | func enable_connection() -> FirestoreListenerConnection: 19 | _enabled = true 20 | set_process(true) 21 | return FirestoreListenerConnection.new(self) 22 | 23 | func _process(delta: float) -> void: 24 | if _enabled: 25 | _total_time += delta 26 | if _total_time >= _poll_time: 27 | _check_for_server_updates() 28 | _total_time = 0.0 29 | 30 | func _check_for_server_updates() -> void: 31 | var executor = func(): 32 | var doc = await _collection.get_doc(_doc_name, false, true) 33 | if doc == null: 34 | set_process(false) # Document was deleted out from under us, so stop updating 35 | 36 | executor.call() # Hack to work around the await here, otherwise would have to call with await in _process and that's no bueno 37 | 38 | class FirestoreListenerConnection extends RefCounted: 39 | var connection 40 | 41 | func _init(connection_node): 42 | connection = connection_node 43 | 44 | func stop(): 45 | if connection != null and is_instance_valid(connection): 46 | connection.set_process(false) 47 | connection.free() 48 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/firestore_listener.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://bwv7vtgssc0n5"] 2 | 3 | [ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore_listener.gd" id="1_qlaei"] 4 | 5 | [node name="FirestoreListener" type="Node"] 6 | script = ExtResource("1_qlaei") 7 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/firestore_query.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors Nicoló 'fenix' Santilio, Kyle Szklenski 2 | ## @meta-version 1.4 3 | ## A firestore query. 4 | ## Documentation TODO. 5 | @tool 6 | extends RefCounted 7 | class_name FirestoreQuery 8 | 9 | class Order: 10 | var obj: Dictionary 11 | 12 | class Cursor: 13 | var values: Array 14 | var before: bool 15 | 16 | func _init(v : Array,b : bool): 17 | values = v 18 | before = b 19 | 20 | signal query_result(query_result) 21 | 22 | const TEMPLATE_QUERY: Dictionary = { 23 | select = {}, 24 | from = [], 25 | where = {}, 26 | orderBy = [], 27 | startAt = {}, 28 | endAt = {}, 29 | offset = 0, 30 | limit = 0 31 | } 32 | 33 | var query: Dictionary = {} 34 | var aggregations: Array[Dictionary] = [] 35 | var sub_collection_path: String = "" 36 | 37 | enum OPERATOR { 38 | # Standard operators 39 | OPERATOR_UNSPECIFIED, 40 | LESS_THAN, 41 | LESS_THAN_OR_EQUAL, 42 | GREATER_THAN, 43 | GREATER_THAN_OR_EQUAL, 44 | EQUAL, 45 | NOT_EQUAL, 46 | ARRAY_CONTAINS, 47 | ARRAY_CONTAINS_ANY, 48 | IN, 49 | NOT_IN, 50 | 51 | # Unary operators 52 | IS_NAN, 53 | IS_NULL, 54 | IS_NOT_NAN, 55 | IS_NOT_NULL, 56 | 57 | # Complex operators 58 | AND, 59 | OR 60 | } 61 | 62 | enum DIRECTION { 63 | DIRECTION_UNSPECIFIED, 64 | ASCENDING, 65 | DESCENDING 66 | } 67 | 68 | 69 | # Select which fields you want to return as a reflection from your query. 70 | # Fields must be added inside a list. Only a field is accepted inside the list 71 | # Leave the Array empty if you want to return the whole document 72 | func select(fields) -> FirestoreQuery: 73 | match typeof(fields): 74 | TYPE_STRING: 75 | query["select"] = { fields = { fieldPath = fields } } 76 | TYPE_ARRAY: 77 | for field in fields: 78 | field = ({ fieldPath = field }) 79 | query["select"] = { fields = fields } 80 | _: 81 | print("Type of 'fields' is not accepted.") 82 | return self 83 | 84 | 85 | 86 | # Select the collection you want to return the query result from 87 | # if @all_descendants also sub-collections will be returned. If false, only documents will be returned 88 | func from(collection_id : String, all_descendants : bool = true) -> FirestoreQuery: 89 | query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}] 90 | return self 91 | 92 | # @collections_array MUST be an Array of Arrays with this structure 93 | # [ ["collection_id", true/false] ] 94 | func from_many(collections_array : Array) -> FirestoreQuery: 95 | var collections : Array = [] 96 | for collection in collections_array: 97 | collections.append({collectionId = collection[0], allDescendants = collection[1]}) 98 | query["from"] = collections.duplicate(true) 99 | return self 100 | 101 | 102 | # Query the value of a field you want to match 103 | # @field : the name of the field 104 | # @operator : from FirestoreQuery.OPERATOR 105 | # @value : can be any type - String, int, bool, float 106 | # @chain : from FirestoreQuery.OPERATOR.[OR/AND], use it only if you want to chain "AND" or "OR" logic with futher where() calls 107 | # eg. super.where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20) 108 | func where(field : String, operator : int, value = null, chain : int = -1): 109 | if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]: 110 | if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): 111 | var filters : Array = [] 112 | if query.has("where") and query.where.has("compositeFilter"): 113 | if chain == -1: 114 | filters = query.where.compositeFilter.filters.duplicate(true) 115 | chain = OPERATOR.get(query.where.compositeFilter.op) 116 | else: 117 | filters.append(query.where) 118 | filters.append(create_unary_filter(field, operator)) 119 | query["where"] = create_composite_filter(chain, filters) 120 | else: 121 | query["where"] = create_unary_filter(field, operator) 122 | else: 123 | if value == null: 124 | print("A value must be defined to match the field: {field}".format({field = field})) 125 | else: 126 | if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")): 127 | var filters : Array = [] 128 | if query.has("where") and query.where.has("compositeFilter"): 129 | if chain == -1: 130 | filters = query.where.compositeFilter.filters.duplicate(true) 131 | chain = OPERATOR.get(query.where.compositeFilter.op) 132 | else: 133 | filters.append(query.where) 134 | filters.append(create_field_filter(field, operator, value)) 135 | query["where"] = create_composite_filter(chain, filters) 136 | else: 137 | query["where"] = create_field_filter(field, operator, value) 138 | return self 139 | 140 | 141 | # Order by a field, defining its name and the order direction 142 | # default directoin = Ascending 143 | func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> FirestoreQuery: 144 | query["orderBy"] = [_order_object(field, direction).obj] 145 | return self 146 | 147 | 148 | # Order by a set of fields and directions 149 | # @order_list is an Array of Arrays with the following structure 150 | # [@field_name , @DIRECTION.[direction]] 151 | # else, order_object() can be called to return an already parsed Dictionary 152 | func order_by_fields(order_field_list : Array) -> FirestoreQuery: 153 | var order_list : Array = [] 154 | for order in order_field_list: 155 | if order is Array: 156 | order_list.append(_order_object(order[0], order[1]).obj) 157 | elif order is Order: 158 | order_list.append(order.obj) 159 | query["orderBy"] = order_list 160 | return self 161 | 162 | func start_at(value, before : bool) -> FirestoreQuery: 163 | var cursor : Cursor = _cursor_object(value, before) 164 | query["startAt"] = { values = cursor.values, before = cursor.before } 165 | print(query["startAt"]) 166 | return self 167 | 168 | 169 | func end_at(value, before : bool) -> FirestoreQuery: 170 | var cursor : Cursor = _cursor_object(value, before) 171 | query["startAt"] = { values = cursor.values, before = cursor.before } 172 | print(query["startAt"]) 173 | return self 174 | 175 | 176 | func offset(offset : int) -> FirestoreQuery: 177 | if offset < 0: 178 | print("If specified, offset must be >= 0") 179 | else: 180 | query["offset"] = offset 181 | return self 182 | 183 | 184 | func limit(limit : int) -> FirestoreQuery: 185 | if limit < 0: 186 | print("If specified, offset must be >= 0") 187 | else: 188 | query["limit"] = limit 189 | return self 190 | 191 | 192 | func aggregate() -> FirestoreAggregation: 193 | return FirestoreAggregation.new(self) 194 | 195 | class FirestoreAggregation extends RefCounted: 196 | var _query: FirestoreQuery 197 | 198 | func _init(query: FirestoreQuery) -> void: 199 | _query = query 200 | 201 | func sum(field: String) -> FirestoreQuery: 202 | _query.aggregations.push_back({ sum = { field = { fieldPath = field }}}) 203 | return _query 204 | 205 | func count(up_to: int) -> FirestoreQuery: 206 | _query.aggregations.push_back({ count = { upTo = up_to }}) 207 | return _query 208 | 209 | func average(field: String) -> FirestoreQuery: 210 | _query.aggregations.push_back({ avg = { field = { fieldPath = field }}}) 211 | return _query 212 | 213 | # UTILITIES ---------------------------------------- 214 | 215 | static func _cursor_object(value, before : bool) -> Cursor: 216 | var parse : Dictionary = Utilities.dict2fields({value = value}).fields.value 217 | var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before) 218 | return cursor 219 | 220 | static func _order_object(field : String, direction : int) -> Order: 221 | var order : Order = Order.new() 222 | order.obj = { field = { fieldPath = field }, direction = DIRECTION.keys()[direction] } 223 | return order 224 | 225 | 226 | func create_field_filter(field : String, operator : int, value) -> Dictionary: 227 | return { 228 | fieldFilter = { 229 | field = { fieldPath = field }, 230 | op = OPERATOR.keys()[operator], 231 | value = Utilities.dict2fields({value = value}).fields.value 232 | } } 233 | 234 | func create_unary_filter(field : String, operator : int) -> Dictionary: 235 | return { 236 | unaryFilter = { 237 | field = { fieldPath = field }, 238 | op = OPERATOR.keys()[operator], 239 | } } 240 | 241 | func create_composite_filter(operator : int, filters : Array) -> Dictionary: 242 | return { 243 | compositeFilter = { 244 | op = OPERATOR.keys()[operator], 245 | filters = filters 246 | } } 247 | 248 | func clean() -> void: 249 | query = { } 250 | 251 | func _to_string() -> String: 252 | var pretty : String = "QUERY:\n" 253 | for key in query.keys(): 254 | pretty += "- {key} = {value}\n".format({key = key, value = query.get(key)}) 255 | return pretty 256 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/firestore_task.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors Nicolò 'fenix' Santilio, Kyle 'backat50ft' Szklenski 2 | ## @meta-version 1.4 3 | ## 4 | ## A [code]FirestoreTask[/code] is an independent node inheriting [code]HTTPRequest[/code] that processes a [code]Firestore[/code] request. 5 | ## 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. 6 | ## 7 | ## Being a [code]Node[/code] it can be stored in a variable to yield checked it, and receive its result as a callback. 8 | ## 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]). 9 | ## An indirect level of communication with Tasks is also provided, redirecting signals to the [class FirebaseFirestore] module. 10 | ## 11 | ## ex. 12 | ## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code] 13 | ## [code]var result : Array = await task.task_finished[/code] 14 | ## [code]var result : Array = await task.result_query[/code] 15 | ## [code]var result : Array = await Firebase.Firestore.task_finished[/code] 16 | ## [code]var result : Array = await Firebase.Firestore.result_query[/code] 17 | ## 18 | ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask 19 | 20 | @tool 21 | class_name FirestoreTask 22 | extends RefCounted 23 | 24 | ## 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. 25 | ## @arg-types Variant 26 | signal task_finished() 27 | 28 | enum Task { 29 | TASK_GET, ## A GET Request Task, processing a get() request 30 | TASK_POST, ## A POST Request Task, processing add() request 31 | TASK_PATCH, ## A PATCH Request Task, processing a update() request 32 | TASK_DELETE, ## A DELETE Request Task, processing a delete() request 33 | TASK_QUERY, ## A POST Request Task, processing a query() request 34 | TASK_AGG_QUERY, ## A POST Request Task, processing an aggregation_query() request 35 | TASK_LIST, ## A POST Request Task, processing a list() request 36 | TASK_COMMIT ## A POST Request Task that hits the write api 37 | } 38 | 39 | ## Mapping of Task enum values to descriptions for use in printing user-friendly error codes. 40 | const TASK_MAP = { 41 | Task.TASK_GET: "GET DOCUMENT", 42 | Task.TASK_POST: "ADD DOCUMENT", 43 | Task.TASK_PATCH: "UPDATE DOCUMENT", 44 | Task.TASK_DELETE: "DELETE DOCUMENT", 45 | Task.TASK_QUERY: "QUERY COLLECTION", 46 | Task.TASK_LIST: "LIST DOCUMENTS", 47 | Task.TASK_COMMIT: "COMMIT DOCUMENT", 48 | Task.TASK_AGG_QUERY: "AGG QUERY COLLECTION" 49 | } 50 | 51 | ## The code indicating the request Firestore is processing. 52 | ## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers. 53 | ## @setter set_action 54 | var action : int = -1 : set = set_action 55 | 56 | ## A variable, temporary holding the result of the request. 57 | var data 58 | var error: Dictionary 59 | var document: FirestoreDocument 60 | 61 | var _response_headers: PackedStringArray = PackedStringArray() 62 | var _response_code: int = 0 63 | 64 | var _method: int = -1 65 | var _url: String = "" 66 | var _fields: String = "" 67 | var _headers: PackedStringArray = [] 68 | 69 | func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: 70 | var bod = body.get_string_from_utf8() 71 | if bod != "": 72 | bod = Utilities.get_json_data(bod) 73 | 74 | var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK 75 | # Probably going to regret this... 76 | if response_code == HTTPClient.RESPONSE_OK: 77 | match action: 78 | Task.TASK_POST, Task.TASK_GET, Task.TASK_PATCH: 79 | document = FirestoreDocument.new(bod) 80 | data = document 81 | Task.TASK_DELETE: 82 | data = true 83 | Task.TASK_QUERY: 84 | data = [] 85 | for doc in bod: 86 | if doc.has('document'): 87 | data.append(FirestoreDocument.new(doc.document)) 88 | Task.TASK_AGG_QUERY: 89 | var agg_results = [] 90 | for agg_result in bod: 91 | var idx = 0 92 | var query_results = {} 93 | for field_value in agg_result.result.aggregateFields.keys(): 94 | var agg = data.aggregations[idx] 95 | var field = agg_result.result.aggregateFields[field_value] 96 | query_results[agg.keys()[0]] = Utilities.from_firebase_type(field) 97 | idx += 1 98 | agg_results.push_back(query_results) 99 | data = agg_results 100 | Task.TASK_LIST: 101 | data = [] 102 | if bod.has('documents'): 103 | for doc in bod.documents: 104 | data.append(FirestoreDocument.new(doc)) 105 | if bod.has("nextPageToken"): 106 | data.append(bod.nextPageToken) 107 | Task.TASK_COMMIT: 108 | data = bod # Commit's response is not a full document, so don't treat it as such 109 | else: 110 | var description = "" 111 | if TASK_MAP.has(action): 112 | description = "(" + TASK_MAP[action] + ")" 113 | 114 | Firebase._printerr("Action in error was: " + str(action) + " " + description) 115 | build_error(bod, action, description) 116 | 117 | task_finished.emit() 118 | 119 | func build_error(_error, action, description) -> void: 120 | if _error: 121 | if _error is Array and _error.size() > 0 and _error[0].has("error"): 122 | _error = _error[0].error 123 | elif _error is Dictionary and _error.keys().size() > 0 and _error.has("error"): 124 | _error = _error.error 125 | 126 | error = _error 127 | else: 128 | #error.code, error.status, error.message 129 | error = { "error": { 130 | "code": 0, 131 | "status": "Unknown Error", 132 | "message": "Error: %s - %s" % [action, description] 133 | } 134 | } 135 | 136 | data = null 137 | 138 | func set_action(value : int) -> void: 139 | action = value 140 | match action: 141 | Task.TASK_GET, Task.TASK_LIST: 142 | _method = HTTPClient.METHOD_GET 143 | Task.TASK_POST, Task.TASK_QUERY, Task.TASK_AGG_QUERY: 144 | _method = HTTPClient.METHOD_POST 145 | Task.TASK_PATCH: 146 | _method = HTTPClient.METHOD_PATCH 147 | Task.TASK_DELETE: 148 | _method = HTTPClient.METHOD_DELETE 149 | Task.TASK_COMMIT: 150 | _method = HTTPClient.METHOD_POST 151 | _: 152 | assert(false) 153 | 154 | 155 | func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary: 156 | var ret := dic_a.duplicate(true) 157 | for key in dic_b: 158 | var val = dic_b[key] 159 | 160 | if val == null and nullify: 161 | ret.erase(key) 162 | elif val is Array: 163 | ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val) 164 | elif val is Dictionary: 165 | ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val) 166 | else: 167 | ret[key] = val 168 | return ret 169 | 170 | 171 | func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array: 172 | var ret := arr_a.duplicate(true) 173 | ret.resize(len(arr_b)) 174 | 175 | var deletions := 0 176 | for i in len(arr_b): 177 | var index : int = i - deletions 178 | var val = arr_b[index] 179 | if val == null and nullify: 180 | ret.remove_at(index) 181 | deletions += i 182 | elif val is Array: 183 | ret[index] = _merge_array(ret[index] if ret[index] else [], val) 184 | elif val is Dictionary: 185 | ret[index] = _merge_dict(ret[index] if ret[index] else {}, val) 186 | else: 187 | ret[index] = val 188 | return ret 189 | -------------------------------------------------------------------------------- /addons/godot-firebase/firestore/firestore_transform.gd: -------------------------------------------------------------------------------- 1 | class_name FirestoreTransform 2 | extends RefCounted 3 | 4 | -------------------------------------------------------------------------------- /addons/godot-firebase/functions/function_task.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors Nicolò 'fenix' Santilio, 2 | ## @meta-version 1.2 3 | ## 4 | ## ex. 5 | ## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code] 6 | ## [code]var result : Array = await task.task_finished[/code] 7 | ## [code]var result : Array = await task.result_query[/code] 8 | ## [code]var result : Array = await Firebase.Firestore.task_finished[/code] 9 | ## [code]var result : Array = await Firebase.Firestore.result_query[/code] 10 | ## 11 | ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask 12 | 13 | @tool 14 | class_name FunctionTask 15 | extends RefCounted 16 | 17 | ## 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. 18 | ## @arg-types Variant 19 | signal task_finished(result) 20 | 21 | ## Emitted when a cloud function is correctly executed, returning the Response Code and Result Body 22 | ## @arg-types FirestoreDocument 23 | signal function_executed(response, result) 24 | 25 | ## Emitted when a request is [b]not[/b] successfully completed. 26 | ## @arg-types Dictionary 27 | signal task_error(code, status, message) 28 | 29 | ## A variable, temporary holding the result of the request. 30 | var data: Dictionary 31 | var error: Dictionary 32 | 33 | ## Whether the data came from cache. 34 | var from_cache : bool = false 35 | 36 | var _response_headers : PackedStringArray = PackedStringArray() 37 | var _response_code : int = 0 38 | 39 | var _method : int = -1 40 | var _url : String = "" 41 | var _fields : String = "" 42 | var _headers : PackedStringArray = [] 43 | 44 | func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void: 45 | var bod = Utilities.get_json_data(body) 46 | if bod == null: 47 | bod = {content = body.get_string_from_utf8()} # I don't understand what this line does at all. What the hell?! 48 | 49 | var offline: bool = typeof(bod) == TYPE_NIL 50 | from_cache = offline 51 | 52 | data = bod 53 | if response_code == HTTPClient.RESPONSE_OK and data!=null: 54 | function_executed.emit(result, data) 55 | else: 56 | error = {result=result, response_code=response_code, data=data} 57 | task_error.emit(result, response_code, str(data)) 58 | 59 | task_finished.emit(data) 60 | -------------------------------------------------------------------------------- /addons/godot-firebase/functions/functions.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors Nicolò 'fenix' Santilio, 2 | ## @meta-version 2.5 3 | ## 4 | ## (source: [url=https://firebase.google.com/docs/functions]Functions[/url]) 5 | ## 6 | ## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Functions 7 | @tool 8 | class_name FirebaseFunctions 9 | extends Node 10 | 11 | ## Emitted when a [code]query()[/code] request is successfully completed. [code]error()[/code] signal will be emitted otherwise. 12 | ## @arg-types Array 13 | ## Emitted when a [code]list()[/code] or [code]query()[/code] request is [b]not[/b] successfully completed. 14 | signal task_error(code,status,message) 15 | 16 | # TODO: Implement cache size limit 17 | const CACHE_SIZE_UNLIMITED = -1 18 | 19 | const _CACHE_EXTENSION : String = ".fscache" 20 | const _CACHE_RECORD_FILE : String = "RmlyZXN0b3JlIGNhY2hlLXJlY29yZHMu.fscache" 21 | 22 | const _AUTHORIZATION_HEADER : String = "Authorization: Bearer " 23 | 24 | const _MAX_POOLED_REQUEST_AGE = 30 25 | 26 | ## The code indicating the request Firestore is processing. 27 | ## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers. 28 | ## @enum Requests 29 | var request : int = -1 30 | 31 | ## Whether cache files can be used and generated. 32 | ## @default true 33 | var persistence_enabled : bool = false 34 | 35 | ## Whether an internet connection can be used. 36 | ## @default true 37 | var networking: bool = true : set = set_networking 38 | 39 | ## A Dictionary containing all authentication fields for the current logged user. 40 | ## @type Dictionary 41 | var auth : Dictionary 42 | 43 | var _config : Dictionary = {} 44 | var _cache_loc: String 45 | var _encrypt_key: String = "" if Utilities.is_web() else OS.get_unique_id() 46 | 47 | var _base_url : String = "" 48 | 49 | var _http_request_pool : Array = [] 50 | 51 | var _offline: bool = false : set = _set_offline 52 | 53 | func _ready() -> void: 54 | set_process(false) 55 | 56 | func _process(delta : float) -> void: 57 | for i in range(_http_request_pool.size() - 1, -1, -1): 58 | var request = _http_request_pool[i] 59 | if not request.get_meta("requesting"): 60 | var lifetime: float = request.get_meta("lifetime") + delta 61 | if lifetime > _MAX_POOLED_REQUEST_AGE: 62 | request.queue_free() 63 | _http_request_pool.remove_at(i) 64 | return # Prevent setting a value on request after it's already been queue_freed 65 | request.set_meta("lifetime", lifetime) 66 | 67 | 68 | ## @args 69 | ## @return FunctionTask 70 | func execute(function: String, method: int, params: Dictionary = {}, body: Dictionary = {}) -> FunctionTask: 71 | set_process(true) 72 | var function_task : FunctionTask = FunctionTask.new() 73 | function_task.task_error.connect(_on_task_error) 74 | function_task.task_finished.connect(_on_task_finished) 75 | function_task.function_executed.connect(_on_function_executed) 76 | 77 | function_task._method = method 78 | 79 | var url : String = _base_url + ("/" if not _base_url.ends_with("/") else "") + function 80 | 81 | if not params.is_empty(): 82 | url += "?" 83 | for key in params.keys(): 84 | url += key + "=" + params[key] + "&" 85 | 86 | function_task._url = url 87 | 88 | if not body.is_empty(): 89 | function_task._fields = JSON.stringify(body) 90 | 91 | _pooled_request(function_task) 92 | return function_task 93 | 94 | 95 | func set_networking(value: bool) -> void: 96 | if value: 97 | enable_networking() 98 | else: 99 | disable_networking() 100 | 101 | 102 | func enable_networking() -> void: 103 | if networking: 104 | return 105 | networking = true 106 | _base_url = _base_url.replace("storeoffline", "functions") 107 | 108 | 109 | func disable_networking() -> void: 110 | if not networking: 111 | return 112 | networking = false 113 | # Pointing to an invalid url should do the trick. 114 | _base_url = _base_url.replace("functions", "storeoffline") 115 | 116 | 117 | func _set_offline(value: bool) -> void: 118 | if value == _offline: 119 | return 120 | 121 | _offline = value 122 | if not persistence_enabled: 123 | return 124 | 125 | return 126 | 127 | 128 | func _set_config(config_json : Dictionary) -> void: 129 | _config = config_json 130 | _cache_loc = _config["cacheLocation"] 131 | 132 | if _encrypt_key == "": _encrypt_key = _config.apiKey 133 | _check_emulating() 134 | 135 | 136 | func _check_emulating() -> void : 137 | ## Check emulating 138 | if not Firebase.emulating: 139 | _base_url = "https://{zone}-{projectId}.cloudfunctions.net/".format({ zone = _config.functionsGeoZone, projectId = _config.projectId }) 140 | else: 141 | var port : String = _config.emulators.ports.functions 142 | if port == "": 143 | Firebase._printerr("You are in 'emulated' mode, but the port for Cloud Functions has not been configured.") 144 | else: 145 | _base_url = "http://localhost:{port}/{projectId}/{zone}/".format({ port = port, zone = _config.functionsGeoZone, projectId = _config.projectId }) 146 | 147 | 148 | func _pooled_request(task : FunctionTask) -> void: 149 | if _offline: 150 | task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray()) 151 | return 152 | 153 | if auth == null or auth.is_empty(): 154 | Firebase._print("Unauthenticated request issued...") 155 | Firebase.Auth.login_anonymous() 156 | var result : Array = await Firebase.Auth.auth_request 157 | if result[0] != 1: 158 | _check_auth_error(result[0], result[1]) 159 | Firebase._print("Client connected as Anonymous") 160 | 161 | 162 | task._headers = ["Content-Type: application/json", _AUTHORIZATION_HEADER + auth.idtoken] 163 | 164 | var http_request : HTTPRequest 165 | for request in _http_request_pool: 166 | if not request.get_meta("requesting"): 167 | http_request = request 168 | break 169 | 170 | if not http_request: 171 | http_request = HTTPRequest.new() 172 | Utilities.fix_http_request(http_request) 173 | http_request.accept_gzip = false 174 | _http_request_pool.append(http_request) 175 | add_child(http_request) 176 | http_request.request_completed.connect(_on_pooled_request_completed.bind(http_request)) 177 | 178 | http_request.set_meta("requesting", true) 179 | http_request.set_meta("lifetime", 0.0) 180 | http_request.set_meta("task", task) 181 | http_request.request(task._url, task._headers, task._method, task._fields) 182 | 183 | 184 | # ------------- 185 | 186 | func _on_task_finished(data : Dictionary) : 187 | pass 188 | 189 | func _on_function_executed(result : int, data : Dictionary) : 190 | pass 191 | 192 | func _on_task_error(code : int, status : int, message : String): 193 | task_error.emit(code, status, message) 194 | Firebase._printerr(message) 195 | 196 | func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: 197 | auth = auth_result 198 | 199 | 200 | func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: 201 | auth = auth_result 202 | 203 | 204 | func _on_pooled_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray, request : HTTPRequest) -> void: 205 | request.get_meta("task")._on_request_completed(result, response_code, headers, body) 206 | request.set_meta("requesting", false) 207 | 208 | 209 | func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void: 210 | _set_offline(result != HTTPRequest.RESULT_SUCCESS) 211 | 212 | 213 | func _on_FirebaseAuth_logout() -> void: 214 | auth = {} 215 | 216 | func _check_auth_error(code : int, message : String) -> void: 217 | var err : String 218 | match code: 219 | 400: err = "Please, enable Anonymous Sign-in method or Authenticate the Client before issuing a request (best option)" 220 | Firebase._printerr(err) 221 | -------------------------------------------------------------------------------- /addons/godot-firebase/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /addons/godot-firebase/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://2selq12fp4q0" 6 | path="res://.godot/imported/icon.svg-5c4f39d37c9275a3768de73a392fd315.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/godot-firebase/icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-5c4f39d37c9275a3768de73a392fd315.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/godot-firebase/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="GodotFirebase" 4 | description="Google Firebase SDK written in GDScript for use in Godot Engine 4.x projects." 5 | author="GodotNutsOrg" 6 | version="2.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/godot-firebase/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enable_plugin() -> void: 5 | add_autoload_singleton("Firebase", "res://addons/godot-firebase/firebase/firebase.tscn") 6 | 7 | func _disable_plugin() -> void: 8 | remove_autoload_singleton("Firebase") 9 | -------------------------------------------------------------------------------- /addons/godot-firebase/queues/queueable_http_request.gd: -------------------------------------------------------------------------------- 1 | class_name QueueableHTTPRequest 2 | extends HTTPRequest 3 | 4 | signal queue_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) 5 | 6 | var _queue := [] 7 | 8 | # 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. 9 | 10 | func _ready() -> void: 11 | request_completed.connect( 12 | func(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray): 13 | queue_request_completed.emit(result, response_code, headers, body) 14 | 15 | if not _queue.is_empty(): 16 | var req = _queue.pop_front() 17 | self.request(req.url, req.headers, req.method, req.data) 18 | ) 19 | 20 | func request(url : String, headers : PackedStringArray = PackedStringArray(), method := HTTPClient.METHOD_GET, data : String = "") -> Error: 21 | var status = get_http_client_status() 22 | var result = OK 23 | 24 | if status != HTTPClient.STATUS_DISCONNECTED: 25 | _queue.push_back({url=url, headers=headers, method=method, data=data}) 26 | return result 27 | 28 | result = super.request(url, headers, method, data) 29 | 30 | return result 31 | -------------------------------------------------------------------------------- /addons/godot-firebase/queues/queueable_http_request.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://ctb4l7plg8kqg"] 2 | 3 | [ext_resource type="Script" path="res://addons/godot-firebase/queues/queueable_http_request.gd" id="1_2rucc"] 4 | 5 | [node name="QueueableHTTPRequest" type="HTTPRequest"] 6 | script = ExtResource("1_2rucc") 7 | -------------------------------------------------------------------------------- /addons/godot-firebase/remote_config/firebase_remote_config.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FirebaseRemoteConfig 3 | extends Node 4 | 5 | const RemoteConfigFunctionId = "getRemoteConfig" 6 | 7 | signal remote_config_received(config) 8 | signal remote_config_error(error) 9 | 10 | var _project_config = {} 11 | var _headers : PackedStringArray = [ 12 | ] 13 | var _auth : Dictionary 14 | 15 | func _set_config(config_json : Dictionary) -> void: 16 | _project_config = config_json # This may get confusing, hoping the variable name makes it easier to understand 17 | 18 | func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void: 19 | _auth = auth_result 20 | 21 | func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: 22 | _auth = auth_result 23 | 24 | func _on_FirebaseAuth_logout() -> void: 25 | _auth = {} 26 | 27 | func get_remote_config() -> void: 28 | var function_task = Firebase.Functions.execute("getRemoteConfig", HTTPClient.METHOD_GET, {}, {}) as FunctionTask 29 | var result = await function_task.task_finished 30 | Firebase._print("Config request result: " + str(result)) 31 | if result.has("error"): 32 | remote_config_error.emit(result) 33 | return 34 | 35 | var config = RemoteConfig.new(result) 36 | remote_config_received.emit(config) 37 | -------------------------------------------------------------------------------- /addons/godot-firebase/remote_config/firebase_remote_config.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://5xa6ulbllkjk"] 2 | 3 | [ext_resource type="Script" path="res://addons/godot-firebase/remote_config/firebase_remote_config.gd" id="1_wx4ds"] 4 | 5 | [node name="FirebaseRemoteConfig" type="HTTPRequest"] 6 | use_threads = true 7 | script = ExtResource("1_wx4ds") 8 | -------------------------------------------------------------------------------- /addons/godot-firebase/remote_config/remote_config.gd: -------------------------------------------------------------------------------- 1 | class_name RemoteConfig 2 | extends RefCounted 3 | 4 | var default_config = {} 5 | 6 | func _init(values : Dictionary) -> void: 7 | default_config = values 8 | 9 | func get_value(key : String) -> Variant: 10 | if default_config.has(key): 11 | return default_config[key] 12 | 13 | Firebase._printerr("Remote config does not contain key: " + key) 14 | return null 15 | -------------------------------------------------------------------------------- /addons/godot-firebase/storage/storage.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors SIsilicon 2 | ## @meta-version 2.2 3 | ## The Storage API for Firebase. 4 | ## 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. 5 | ## 6 | ## [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] 7 | @tool 8 | class_name FirebaseStorage 9 | extends Node 10 | 11 | const _API_VERSION : String = "v0" 12 | 13 | ## @arg-types int, int, PackedStringArray 14 | ## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode 15 | ## Emitted when a [StorageTask] has finished with an error. 16 | signal task_failed(result, response_code, data) 17 | 18 | ## The current storage bucket the Storage API is referencing. 19 | var bucket : String 20 | 21 | ## @default false 22 | ## Whether a task is currently being processed. 23 | var requesting : bool = false 24 | 25 | var _auth : Dictionary 26 | var _config : Dictionary 27 | 28 | var _references : Dictionary = {} 29 | 30 | var _base_url : String = "" 31 | var _extended_url : String = "/[API_VERSION]/b/[APP_ID]/o/[FILE_PATH]" 32 | var _root_ref : StorageReference 33 | 34 | var _http_client : HTTPClient = HTTPClient.new() 35 | var _pending_tasks : Array = [] 36 | 37 | var _current_task : StorageTask 38 | var _response_code : int 39 | var _response_headers : PackedStringArray 40 | var _response_data : PackedByteArray 41 | var _content_length : int 42 | var _reading_body : bool 43 | 44 | func _notification(what : int) -> void: 45 | if what == NOTIFICATION_INTERNAL_PROCESS: 46 | _internal_process(get_process_delta_time()) 47 | 48 | func _internal_process(_delta : float) -> void: 49 | if not requesting: 50 | set_process_internal(false) 51 | return 52 | 53 | var task = _current_task 54 | 55 | match _http_client.get_status(): 56 | HTTPClient.STATUS_DISCONNECTED: 57 | _http_client.connect_to_host(_base_url, 443, TLSOptions.client()) # Uhh, check if this is going to work. I assume not. 58 | 59 | HTTPClient.STATUS_RESOLVING, \ 60 | HTTPClient.STATUS_REQUESTING, \ 61 | HTTPClient.STATUS_CONNECTING: 62 | _http_client.poll() 63 | 64 | HTTPClient.STATUS_CONNECTED: 65 | var err := _http_client.request_raw(task._method, task._url, task._headers, task.data) 66 | if err: 67 | _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) 68 | 69 | HTTPClient.STATUS_BODY: 70 | if _http_client.has_response() or _reading_body: 71 | _reading_body = true 72 | 73 | # If there is a response... 74 | if _response_headers.is_empty(): 75 | _response_headers = _http_client.get_response_headers() # Get response headers. 76 | _response_code = _http_client.get_response_code() 77 | 78 | for header in _response_headers: 79 | if "Content-Length" in header: 80 | _content_length = header.trim_prefix("Content-Length: ").to_int() 81 | break 82 | 83 | _http_client.poll() 84 | var chunk = _http_client.read_response_body_chunk() # Get a chunk. 85 | if chunk.size() == 0: 86 | # Got nothing, wait for buffers to fill a bit. 87 | pass 88 | else: 89 | _response_data += chunk # Append to read buffer. 90 | if _content_length != 0: 91 | task.progress = float(_response_data.size()) / _content_length 92 | 93 | if _http_client.get_status() != HTTPClient.STATUS_BODY: 94 | task.progress = 1.0 95 | _finish_request(HTTPRequest.RESULT_SUCCESS) 96 | else: 97 | task.progress = 1.0 98 | _finish_request(HTTPRequest.RESULT_SUCCESS) 99 | 100 | HTTPClient.STATUS_CANT_CONNECT: 101 | _finish_request(HTTPRequest.RESULT_CANT_CONNECT) 102 | HTTPClient.STATUS_CANT_RESOLVE: 103 | _finish_request(HTTPRequest.RESULT_CANT_RESOLVE) 104 | HTTPClient.STATUS_CONNECTION_ERROR: 105 | _finish_request(HTTPRequest.RESULT_CONNECTION_ERROR) 106 | HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: 107 | _finish_request(HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR) 108 | 109 | ## @args path 110 | ## @arg-defaults "" 111 | ## @return StorageReference 112 | ## 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. 113 | func ref(path := "") -> StorageReference: 114 | if _config == null or _config.is_empty(): 115 | return null 116 | 117 | # Create a root storage reference if there's none 118 | # and we're not making one. 119 | if path != "" and not _root_ref: 120 | _root_ref = ref() 121 | 122 | path = _simplify_path(path) 123 | if not _references.has(path): 124 | var ref := StorageReference.new() 125 | _references[path] = ref 126 | ref.bucket = bucket 127 | ref.full_path = path 128 | ref.file_name = path.get_file() 129 | ref.parent = ref(path.path_join("..")) 130 | ref.root = _root_ref 131 | ref.storage = self 132 | add_child(ref) 133 | return ref 134 | else: 135 | return _references[path] 136 | 137 | func _set_config(config_json : Dictionary) -> void: 138 | _config = config_json 139 | if bucket != _config.storageBucket: 140 | bucket = _config.storageBucket 141 | _http_client.close() 142 | _check_emulating() 143 | 144 | 145 | func _check_emulating() -> void : 146 | ## Check emulating 147 | if not Firebase.emulating: 148 | _base_url = "https://firebasestorage.googleapis.com" 149 | else: 150 | var port : String = _config.emulators.ports.storage 151 | if port == "": 152 | Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.") 153 | else: 154 | _base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port }) 155 | 156 | 157 | func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> Variant: 158 | if _is_invalid_authentication(): 159 | Firebase._printerr("Error uploading to storage: Invalid authentication") 160 | return 0 161 | 162 | var task := StorageTask.new() 163 | task.ref = ref 164 | task._url = _get_file_url(ref) 165 | task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD 166 | task._headers = headers 167 | task.data = data 168 | _process_request(task) 169 | return await task.task_finished 170 | 171 | func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Variant: 172 | if _is_invalid_authentication(): 173 | Firebase._printerr("Error downloading from storage: Invalid authentication") 174 | return 0 175 | 176 | var info_task := StorageTask.new() 177 | info_task.ref = ref 178 | info_task._url = _get_file_url(ref) 179 | info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META 180 | _process_request(info_task) 181 | 182 | if url_only or meta_only: 183 | return await info_task.task_finished 184 | 185 | var task := StorageTask.new() 186 | task.ref = ref 187 | task._url = _get_file_url(ref) + "?alt=media&token=" 188 | task.action = StorageTask.Task.TASK_DOWNLOAD 189 | _pending_tasks.append(task) 190 | 191 | var data = await info_task.task_finished 192 | if info_task.result == OK: 193 | task._url += info_task.data.downloadTokens 194 | else: 195 | task.data = info_task.data 196 | task.response_headers = info_task.response_headers 197 | task.response_code = info_task.response_code 198 | task.result = info_task.result 199 | task.finished = true 200 | task.task_finished.emit(null) 201 | task_failed.emit(task.result, task.response_code, task.data) 202 | _pending_tasks.erase(task) 203 | return null 204 | 205 | return await task.task_finished 206 | 207 | func _list(ref : StorageReference, list_all : bool) -> Array: 208 | if _is_invalid_authentication(): 209 | Firebase._printerr("Error getting object list from storage: Invalid authentication") 210 | return [] 211 | 212 | var task := StorageTask.new() 213 | task.ref = ref 214 | task._url = _get_file_url(_root_ref).trim_suffix("/") 215 | task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST 216 | _process_request(task) 217 | return await task.task_finished 218 | 219 | func _delete(ref : StorageReference) -> bool: 220 | if _is_invalid_authentication(): 221 | Firebase._printerr("Error deleting object from storage: Invalid authentication") 222 | return false 223 | 224 | var task := StorageTask.new() 225 | task.ref = ref 226 | task._url = _get_file_url(ref) 227 | task.action = StorageTask.Task.TASK_DELETE 228 | _process_request(task) 229 | var data = await task.task_finished 230 | 231 | return data == null 232 | 233 | func _process_request(task : StorageTask) -> void: 234 | if requesting: 235 | _pending_tasks.append(task) 236 | return 237 | requesting = true 238 | 239 | var headers = Array(task._headers) 240 | headers.append("Authorization: Bearer " + _auth.idtoken) 241 | task._headers = PackedStringArray(headers) 242 | 243 | _current_task = task 244 | _response_code = 0 245 | _response_headers = PackedStringArray() 246 | _response_data = PackedByteArray() 247 | _content_length = 0 248 | _reading_body = false 249 | 250 | if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]: 251 | _http_client.close() 252 | set_process_internal(true) 253 | 254 | func _finish_request(result : int) -> void: 255 | var task := _current_task 256 | requesting = false 257 | 258 | task.result = result 259 | task.response_code = _response_code 260 | task.response_headers = _response_headers 261 | 262 | match task.action: 263 | StorageTask.Task.TASK_DOWNLOAD: 264 | task.data = _response_data 265 | 266 | StorageTask.Task.TASK_DELETE: 267 | _references.erase(task.ref.full_path) 268 | for child in get_children(): 269 | if child.full_path == task.ref.full_path: 270 | child.queue_free() 271 | break 272 | if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: 273 | task.data = null 274 | 275 | StorageTask.Task.TASK_DOWNLOAD_URL: 276 | var json = Utilities.get_json_data(_response_data) 277 | if json != null and json.has("error"): 278 | Firebase._printerr("Error getting object download url: "+json["error"].message) 279 | if json != null and json.has("downloadTokens"): 280 | task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens 281 | else: 282 | task.data = "" 283 | 284 | StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL: 285 | var json = Utilities.get_json_data(_response_data) 286 | var items := [] 287 | if json != null and json.has("error"): 288 | Firebase._printerr("Error getting data from storage: "+json["error"].message) 289 | if json != null and json.has("items"): 290 | for item in json.items: 291 | var item_name : String = item.name 292 | if item.bucket != bucket: 293 | continue 294 | if not item_name.begins_with(task.ref.full_path): 295 | continue 296 | if task.action == StorageTask.Task.TASK_LIST: 297 | var dir_path : Array = item_name.split("/") 298 | var slash_count : int = task.ref.full_path.count("/") 299 | item_name = "" 300 | for i in slash_count + 1: 301 | item_name += dir_path[i] 302 | if i != slash_count and slash_count != 0: 303 | item_name += "/" 304 | if item_name in items: 305 | continue 306 | 307 | items.append(item_name) 308 | task.data = items 309 | 310 | _: 311 | var json = Utilities.get_json_data(_response_data) 312 | task.data = json 313 | 314 | var next_task = _get_next_pending_task() 315 | 316 | task.finished = true 317 | 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. 318 | if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"): 319 | task_failed.emit(task.result, task.response_code, task.data) 320 | 321 | if next_task and not next_task.finished: 322 | _process_request(next_task) 323 | 324 | func _get_next_pending_task() -> StorageTask: 325 | if _pending_tasks.is_empty(): 326 | return null 327 | 328 | return _pending_tasks.pop_front() 329 | 330 | func _get_file_url(ref : StorageReference) -> String: 331 | var url := _extended_url.replace("[APP_ID]", ref.bucket) 332 | url = url.replace("[API_VERSION]", _API_VERSION) 333 | return url.replace("[FILE_PATH]", ref.full_path.uri_encode()) 334 | 335 | # Removes any "../" or "./" in the file path. 336 | func _simplify_path(path : String) -> String: 337 | var dirs := path.split("/") 338 | var new_dirs := [] 339 | for dir in dirs: 340 | if dir == "..": 341 | new_dirs.pop_back() 342 | elif dir == ".": 343 | pass 344 | else: 345 | new_dirs.push_back(dir) 346 | 347 | var new_path := "/".join(PackedStringArray(new_dirs)) 348 | new_path = new_path.replace("//", "/") 349 | new_path = new_path.replace("\\", "/") 350 | return new_path 351 | 352 | func _on_FirebaseAuth_login_succeeded(auth_token : Dictionary) -> void: 353 | _auth = auth_token 354 | 355 | func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void: 356 | _auth = auth_result 357 | 358 | func _on_FirebaseAuth_logout() -> void: 359 | _auth = {} 360 | 361 | func _is_invalid_authentication() -> bool: 362 | return (_config == null or _config.is_empty()) or (_auth == null or _auth.is_empty()) 363 | -------------------------------------------------------------------------------- /addons/godot-firebase/storage/storage_reference.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors SIsilicon 2 | ## @meta-version 2.2 3 | ## A reference to a file or folder in the Firebase cloud storage. 4 | ## 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. 5 | @tool 6 | class_name StorageReference 7 | extends Node 8 | 9 | ## The default MIME type to use when uploading a file. 10 | ## 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. 11 | const DEFAULT_MIME_TYPE = "application/octet-stream" 12 | 13 | ## A dictionary of common MIME types based checked a file extension. 14 | ## Example: [code]MIME_TYPES.png[/code] will return [code]image/png[/code]. 15 | const MIME_TYPES = { 16 | "bmp": "image/bmp", 17 | "css": "text/css", 18 | "csv": "text/csv", 19 | "gd": "text/plain", 20 | "htm": "text/html", 21 | "html": "text/html", 22 | "jpeg": "image/jpeg", 23 | "jpg": "image/jpeg", 24 | "json": "application/json", 25 | "mp3": "audio/mpeg", 26 | "mpeg": "video/mpeg", 27 | "ogg": "audio/ogg", 28 | "ogv": "video/ogg", 29 | "png": "image/png", 30 | "shader": "text/plain", 31 | "svg": "image/svg+xml", 32 | "tif": "image/tiff", 33 | "tiff": "image/tiff", 34 | "tres": "text/plain", 35 | "tscn": "text/plain", 36 | "txt": "text/plain", 37 | "wav": "audio/wav", 38 | "webm": "video/webm", 39 | "webp": "image/webp", 40 | "xml": "text/xml", 41 | } 42 | 43 | ## @default "" 44 | ## The stroage bucket this referenced file/folder is located in. 45 | var bucket : String = "" 46 | 47 | ## @default "" 48 | ## The path to the file/folder relative to [member bucket]. 49 | var full_path : String = "" 50 | 51 | ## @default "" 52 | ## The name of the file/folder, including any file extension. 53 | ## Example: If the [member full_path] is [code]images/user/image.png[/code], then the [member name] would be [code]image.png[/code]. 54 | var file_name : String = "" 55 | 56 | ## The parent [StorageReference] one level up the file hierarchy. 57 | ## 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]. 58 | var parent : StorageReference 59 | 60 | ## The root [StorageReference]. 61 | var root : StorageReference 62 | 63 | ## @type FirebaseStorage 64 | ## The Storage API that created this [StorageReference] to begin with. 65 | var storage # FirebaseStorage (Can't static type due to cyclic reference) 66 | 67 | ## @args path 68 | ## @return StorageReference 69 | ## Returns a reference to another [StorageReference] relative to this one. 70 | func child(path : String) -> StorageReference: 71 | return storage.ref(full_path.path_join(path)) 72 | 73 | ## @args data, metadata 74 | ## @return int 75 | ## Makes an attempt to upload data to the referenced file location. Returns Variant 76 | func put_data(data : PackedByteArray, metadata := {}) -> Variant: 77 | if not "Content-Length" in metadata and not Utilities.is_web(): 78 | metadata["Content-Length"] = data.size() 79 | 80 | var headers := [] 81 | for key in metadata: 82 | headers.append("%s: %s" % [key, metadata[key]]) 83 | 84 | return await storage._upload(data, headers, self, false) 85 | 86 | 87 | ## @args data, metadata 88 | ## @return int 89 | ## Like [method put_data], but [code]data[/code] is a [String]. 90 | func put_string(data : String, metadata := {}) -> Variant: 91 | return await put_data(data.to_utf8_buffer(), metadata) 92 | 93 | ## @args file_path, metadata 94 | ## @return int 95 | ## Like [method put_data], but the data comes from a file at [code]file_path[/code]. 96 | func put_file(file_path : String, metadata := {}) -> Variant: 97 | var file := FileAccess.open(file_path, FileAccess.READ) 98 | var data := file.get_buffer(file.get_length()) 99 | 100 | if "Content-Type" in metadata: 101 | metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE) 102 | 103 | return await put_data(data, metadata) 104 | 105 | ## @return Variant 106 | ## Makes an attempt to download the files from the referenced file location. Status checked this task is found in the returned [StorageTask]. 107 | func get_data() -> Variant: 108 | var result = await storage._download(self, false, false) 109 | return result 110 | 111 | ## @return StorageTask 112 | ## Like [method get_data], but the data in the returned [StorageTask] comes in the form of a [String]. 113 | func get_string() -> String: 114 | var task := await get_data() 115 | _on_task_finished(task, "stringify") 116 | return task.data 117 | 118 | ## @return StorageTask 119 | ## 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]. 120 | func get_download_url() -> Variant: 121 | return await storage._download(self, false, true) 122 | 123 | ## @return StorageTask 124 | ## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask]. 125 | func get_metadata() -> Variant: 126 | return await storage._download(self, true, false) 127 | 128 | ## @args metadata 129 | ## @return StorageTask 130 | ## 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]. 131 | func update_metadata(metadata : Dictionary) -> Variant: 132 | var data := JSON.stringify(metadata).to_utf8_buffer() 133 | var headers := PackedStringArray(["Accept: application/json"]) 134 | return await storage._upload(data, headers, self, true) 135 | 136 | ## @return StorageTask 137 | ## 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]. 138 | func list() -> Array: 139 | return await storage._list(self, false) 140 | 141 | ## @return StorageTask 142 | ## 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]. 143 | func list_all() -> Array: 144 | return await storage._list(self, true) 145 | 146 | ## @return StorageTask 147 | ## 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]. 148 | func delete() -> bool: 149 | return await storage._delete(self) 150 | 151 | func _to_string() -> String: 152 | var string := "gs://%s/%s" % [bucket, full_path] 153 | return string 154 | 155 | func _on_task_finished(task : StorageTask, action : String) -> void: 156 | match action: 157 | "stringify": 158 | if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY: 159 | task.data = task.data.get_string_from_utf8() 160 | -------------------------------------------------------------------------------- /addons/godot-firebase/storage/storage_task.gd: -------------------------------------------------------------------------------- 1 | ## @meta-authors SIsilicon, Kyle 'backat50ft' Szklenski 2 | ## @meta-version 2.2 3 | ## An object that keeps track of an operation performed by [StorageReference]. 4 | @tool 5 | class_name StorageTask 6 | extends RefCounted 7 | 8 | enum Task { 9 | TASK_UPLOAD, 10 | TASK_UPLOAD_META, 11 | TASK_DOWNLOAD, 12 | TASK_DOWNLOAD_META, 13 | TASK_DOWNLOAD_URL, 14 | TASK_LIST, 15 | TASK_LIST_ALL, 16 | TASK_DELETE, 17 | TASK_MAX ## The number of [enum Task] constants. 18 | } 19 | 20 | ## Emitted when the task is finished. Returns data depending checked the success and action of the task. 21 | signal task_finished(data) 22 | 23 | ## Boolean to determine if this request involves metadata only 24 | var is_meta : bool 25 | 26 | ## @enum Task 27 | ## @default -1 28 | ## @setter set_action 29 | ## The kind of operation this [StorageTask] is keeping track of. 30 | var action : int = -1 : set = set_action 31 | 32 | var ref # Should not be needed, damnit 33 | 34 | ## @default PackedByteArray() 35 | ## Data that the tracked task will/has returned. 36 | var data = PackedByteArray() # data can be of any type. 37 | 38 | ## @default 0.0 39 | ## The percentage of data that has been received. 40 | var progress : float = 0.0 41 | 42 | ## @default -1 43 | ## @enum HTTPRequest.Result 44 | ## The resulting status of the task. Anyting other than [constant HTTPRequest.RESULT_SUCCESS] means an error has occured. 45 | var result : int = -1 46 | 47 | ## @default false 48 | ## Whether the task is finished processing. 49 | var finished : bool = false 50 | 51 | ## @default PackedStringArray() 52 | ## The returned HTTP response headers. 53 | var response_headers := PackedStringArray() 54 | 55 | ## @default 0 56 | ## @enum HTTPClient.ResponseCode 57 | ## The returned HTTP response code. 58 | var response_code : int = 0 59 | 60 | var _method : int = -1 61 | var _url : String = "" 62 | var _headers : PackedStringArray = PackedStringArray() 63 | 64 | func set_action(value : int) -> void: 65 | action = value 66 | match action: 67 | Task.TASK_UPLOAD: 68 | _method = HTTPClient.METHOD_POST 69 | Task.TASK_UPLOAD_META: 70 | _method = HTTPClient.METHOD_PATCH 71 | Task.TASK_DELETE: 72 | _method = HTTPClient.METHOD_DELETE 73 | _: 74 | _method = HTTPClient.METHOD_GET 75 | -------------------------------------------------------------------------------- /addons/http-sse-client/HTTPSSEClient.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | signal new_sse_event(headers, event, data) 5 | signal connected 6 | signal connection_error(error) 7 | 8 | const event_tag = "event:" 9 | const data_tag = "data:" 10 | const continue_internal = "continue_internal" 11 | 12 | var httpclient = HTTPClient.new() 13 | var is_connected = false 14 | 15 | var domain 16 | var url_after_domain 17 | var port 18 | var trusted_chain 19 | var common_name_override 20 | var told_to_connect = false 21 | var connection_in_progress = false 22 | var is_requested = false 23 | var response_body = PackedByteArray() 24 | 25 | func connect_to_host(domain : String, url_after_domain : String, port : int = -1, trusted_chain : X509Certificate = null, common_name_override : String = ""): 26 | process_mode = Node.PROCESS_MODE_INHERIT 27 | self.domain = domain 28 | self.url_after_domain = url_after_domain 29 | self.port = port 30 | self.trusted_chain = trusted_chain 31 | self.common_name_override = common_name_override 32 | told_to_connect = true 33 | 34 | func attempt_to_connect(): 35 | var tls_options = TLSOptions.client(trusted_chain, common_name_override) 36 | var err = httpclient.connect_to_host(domain, port, tls_options) 37 | if err == OK: 38 | connected.emit() 39 | is_connected = true 40 | else: 41 | connection_error.emit(str(err)) 42 | 43 | func attempt_to_request(httpclient_status): 44 | if httpclient_status == HTTPClient.STATUS_CONNECTING or httpclient_status == HTTPClient.STATUS_RESOLVING: 45 | return 46 | 47 | if httpclient_status == HTTPClient.STATUS_CONNECTED: 48 | var err = httpclient.request(HTTPClient.METHOD_POST, url_after_domain, ["Accept: text/event-stream"]) 49 | if err == OK: 50 | is_requested = true 51 | 52 | func _process(delta): 53 | if !told_to_connect: 54 | return 55 | 56 | if !is_connected: 57 | if !connection_in_progress: 58 | attempt_to_connect() 59 | connection_in_progress = true 60 | return 61 | 62 | httpclient.poll() 63 | var httpclient_status = httpclient.get_status() 64 | if !is_requested: 65 | attempt_to_request(httpclient_status) 66 | return 67 | 68 | if httpclient.has_response() or httpclient_status == HTTPClient.STATUS_BODY: 69 | var headers = httpclient.get_response_headers_as_dictionary() 70 | 71 | if httpclient_status == HTTPClient.STATUS_BODY: 72 | httpclient.poll() 73 | var chunk = httpclient.read_response_body_chunk() 74 | if(chunk.size() == 0): 75 | return 76 | else: 77 | response_body = response_body + chunk 78 | 79 | _parse_response_body(headers) 80 | 81 | elif Firebase.emulating and Firebase._config.workarounds.database_connection_closed_issue: 82 | # Emulation does not send the close connection header currently, so we need to manually read the response body 83 | # see issue https://github.com/firebase/firebase-tools/issues/3329 in firebase-tools 84 | # also comment https://github.com/GodotNuts/GodotFirebase/issues/154#issuecomment-831377763 which explains the issue 85 | while httpclient.connection.get_available_bytes(): 86 | var data = httpclient.connection.get_partial_data(1) 87 | if data[0] == OK: 88 | response_body.append_array(data[1]) 89 | if response_body.size() > 0: 90 | _parse_response_body(headers) 91 | 92 | func _parse_response_body(headers): 93 | var body = response_body.get_string_from_utf8() 94 | if body: 95 | var event_datas = get_event_data(body) 96 | var resize_response_body_to_zero_after_for_loop_flag = false 97 | for event_data in event_datas: 98 | if event_data.event != "keep-alive" and event_data.event != continue_internal: 99 | var result = Utilities.get_json_data(event_data.data) 100 | if result != null: 101 | var parsed_text = result 102 | if response_body.size() > 0: 103 | resize_response_body_to_zero_after_for_loop_flag = true 104 | new_sse_event.emit(headers, event_data.event, result) 105 | else: 106 | if event_data.event != continue_internal: 107 | response_body.resize(0) 108 | if resize_response_body_to_zero_after_for_loop_flag: 109 | response_body.resize(0) 110 | 111 | func get_event_data(body : String) -> Array: 112 | var results = [] 113 | var start_idx = 0 114 | 115 | if body.find(event_tag, start_idx) == -1: 116 | return [{"event":continue_internal}] 117 | 118 | while true: 119 | # Find the index of the next event tag 120 | var event_idx = body.find(event_tag, start_idx) 121 | if event_idx == -1: 122 | break # No more events found 123 | 124 | # Find the index of the corresponding data tag 125 | var data_idx = body.find(data_tag, event_idx + event_tag.length()) 126 | if data_idx == -1: 127 | break # No corresponding data found 128 | 129 | # Extract the event 130 | var event_value = body.substr(event_idx + event_tag.length(), data_idx - (event_idx + event_tag.length())).strip_edges() 131 | if event_value == "": 132 | break # No valid event value found 133 | 134 | # Extract the data 135 | var data_end = body.find(event_tag, data_idx) # Assume data ends at the next event tag 136 | if data_end == -1: 137 | data_end = body.length() # If no new event tag, read till the end of the body 138 | 139 | var data_value = body.substr(data_idx + data_tag.length(), data_end - (data_idx + data_tag.length())).strip_edges() 140 | if data_value == "": 141 | break # No valid data found 142 | 143 | # Append the event and data to results 144 | results.append({"event": event_value, "data": data_value}) 145 | # Update the start index for the next iteration 146 | start_idx = data_end # Move past the current data section 147 | 148 | return results 149 | 150 | func _exit_tree(): 151 | if httpclient: 152 | httpclient.close() 153 | -------------------------------------------------------------------------------- /addons/http-sse-client/HTTPSSEClient.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://addons/http-sse-client/HTTPSSEClient.gd" type="Script" id=1] 4 | 5 | [node name="HTTPSSEClient" type="Node"] 6 | script = ExtResource( 1 ) 7 | -------------------------------------------------------------------------------- /addons/http-sse-client/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kyle Szklenski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/http-sse-client/README.md: -------------------------------------------------------------------------------- 1 | # HTTPSSEClient 2 | 3 | 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. 4 | 5 | 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. 6 | 7 | I've included Demo.tscn and Demo.gd to show the usage of this plugin, and here's a summary: 8 | 9 | 1) Download and place into the proper folder as the above suggests 10 | 2) Switch the new plugin, found in Project Settings -> Plugins, to active 11 | 3) Instantiate a new HTTPSSEClient node in your scene tree somewhere 12 | 4) Click on the script icon for the newly-created node 13 | 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) 14 | 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. 15 | 16 | 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. 17 | -------------------------------------------------------------------------------- /addons/http-sse-client/httpsseclient_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree(): 5 | add_custom_type("HTTPSSEClient", "Node", preload("HTTPSSEClient.gd"), preload("icon.png")) 6 | 7 | func _exit_tree(): 8 | remove_custom_type("HTTPSSEClient") 9 | -------------------------------------------------------------------------------- /addons/http-sse-client/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GodotNuts/GodotFirebase/b99ee8548ec168fb78bbe50063d5f633da8687d0/addons/http-sse-client/icon.png -------------------------------------------------------------------------------- /addons/http-sse-client/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="HTTPSSEClient" 4 | description="An HTTPClient-based implementation that supports server-sent events, effectively enabling push notifications in Godot, using GDScript." 5 | author="Kyle Szklenski" 6 | version="1.1" 7 | script="httpsseclient_plugin.gd" 8 | -------------------------------------------------------------------------------- /ios_plugins/godot_svc/bin 3.x/godot_svc.debug.xcframework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 |