This commit firstly removes the 'logger' variable in CoreBaseModule, secondly renames 'loggeri' to 'logger' in CoreBaseModule, effectively replacing it, and thirdly it forces using 'stringify_variables' onto all log calls.
184 lines
10 KiB
GDScript
184 lines
10 KiB
GDScript
# CORE FRAMEWORK SOURCE FILE
|
|
# Copyright (c) 2024 The StarOpenSource Project & Contributors
|
|
# Licensed under the GNU Affero General Public License v3
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
## Allows for awaited, batched and oneline requests.
|
|
extends CoreBaseModule
|
|
|
|
# Contains a list of all queued downloads.
|
|
var list_queue: Dictionary = {}
|
|
# Contains a list of all active downloads.
|
|
var list_active: Dictionary = {}
|
|
# Contains a liust of all completed downloads.
|
|
var list_complete: Dictionary = {}
|
|
|
|
## Determines how unsecure requests should be handled.
|
|
var config_unsecure_requests: CoreTypes.BlockadeLevel
|
|
|
|
# +++ module +++
|
|
func _cleanup() -> void:
|
|
clean_queue()
|
|
clean_completed()
|
|
for child in get_children():
|
|
if child.is_class("HTTPRequest"):
|
|
child.cancel_request()
|
|
await get_tree().process_frame
|
|
remove_child(child)
|
|
|
|
# +++ methods that do the heavily lifting +++
|
|
## Requests a file from the internet.[br]
|
|
## [br]
|
|
## The returned [code]Dictionary[/code] has the following structure (example):
|
|
## [codeblock]
|
|
## { "result": 0, "http_code": 200, "headers": [ "Server": "nginx" ], "body": [], "body_utf8": [] }
|
|
## ^ ^ ^ ^ ^
|
|
## | | | | |
|
|
## | | | | --------- The body from the server, as a UTF-8 string (set to "" if 'parse_utf8' is false)
|
|
## | | | ----------------------- The body from the server, as bytes
|
|
## | | ------------------------------------------------------ A array of headers
|
|
## | ---------------------------------------------------------------------- The HTTP response code
|
|
## ------------------------------------------------------------------------------------ Equal to @GlobalScope.Error. If not 0/Error.OK = the request failed
|
|
## [/codeblock]
|
|
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
|
|
func awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Dictionary:
|
|
logger.verb("Creating awaited request")
|
|
if !await is_url_allowed(url): return {}
|
|
var id: int = create_request(url, method, headers, data)
|
|
start_request(id, parse_utf8)
|
|
logger.diag("Waiting for request " + str(id) + " to finish")
|
|
logger.diag(core.stringify_variables("Waiting for request %id% to finish", { "id": id }))
|
|
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
|
|
var dldata: Dictionary = list_complete[id]
|
|
list_complete.erase(id)
|
|
return dldata
|
|
|
|
## Requests a file from the internet without returning the godot code, http code or headers. Useful for oneliners.[br]
|
|
## [br]
|
|
## Returns [code]null[/code] on error. To ignore HTTP errors (ie. non-200 statuses) set [code]ignore_http_code[/code] to [code]true[/code].[br]
|
|
## Returns a UTF-8 string with [code]return_utf8[/code] turned on, returns bytes when turned off.[br]
|
|
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
|
|
func oneline_awaited_request(url: String, return_utf8: bool = true, ignore_http_code: bool = false, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Variant:
|
|
var dldata: Dictionary = await awaited_request(url, return_utf8, method, headers, data)
|
|
if dldata == {}: return null
|
|
if dldata["result"] != Error.OK: return null
|
|
elif !ignore_http_code and dldata["http_code"] != 200: return null
|
|
else:
|
|
if return_utf8: return dldata["body_utf8"]
|
|
else: return dldata["body"]
|
|
|
|
## Requests multiple files from the internet.[br]
|
|
## [br]
|
|
## Thee returned [code]Dictionary[/code]s have the following structure (example):
|
|
## [codeblock]
|
|
## { "result": 0, "http_code": 200, "headers": [ "Server": "nginx" ], "body": [], "body_utf8": [] }
|
|
## ^ ^ ^ ^ ^
|
|
## | | | | |
|
|
## | | | | --------- The body from the server, as a UTF-8 string (set to "" if 'parse_utf8' is false)
|
|
## | | | ----------------------- The body from the server, as bytes
|
|
## | | ------------------------------------------------------ A array of headers
|
|
## | ---------------------------------------------------------------------- The HTTP response code
|
|
## ------------------------------------------------------------------------------------ Equal to @GlobalScope.Error. If not 0/Error.OK = the request failed
|
|
## [/codeblock]
|
|
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b]
|
|
func batch_awaited_request(urls: PackedStringArray, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> Array[Dictionary]:
|
|
logger.verb("Creating " + str(urls.size()) + " awaited request(s)")
|
|
var dldata: Array[Dictionary] = []
|
|
for url in urls:
|
|
if !await is_url_allowed(url): continue
|
|
var thread: Thread = Thread.new()
|
|
thread.start(Callable(self, "_batch_awaited_request").bind(url, parse_utf8, method, headers, data))
|
|
var id: int = thread.wait_to_finish()
|
|
dldata.append(list_complete[id])
|
|
list_complete.erase(id)
|
|
return dldata
|
|
|
|
# Does the work, but in a thread.
|
|
func _batch_awaited_request(url: String, parse_utf8: bool, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), data: String = "") -> int:
|
|
var id: int = create_request(url, method, headers, data)
|
|
start_request(id, parse_utf8)
|
|
logger.diag(core.stringify_variables("Waiting for request %id% to finish", { "id": id }))
|
|
while !is_request_completed(id): await get_tree().create_timer(0.1, true).timeout
|
|
return id
|
|
|
|
# +++ internal +++
|
|
# Returns a new download id.
|
|
func generate_id() -> int:
|
|
var id = randi()
|
|
if list_queue.has(id) or list_active.has(id):
|
|
logger.warn(core.stringify_variables("New download id %id% already taken", { "id": id }))
|
|
return generate_id()
|
|
logger.diag(core.stringify_variables("Generated new download id %id%", { "id": id }))
|
|
return id
|
|
|
|
## Creates a new request and stores it in the queue. Returns the download id.[br]
|
|
## [b]Warning: [i]You'll probably not need this. Only use this function when implementing your own downloading method.[/i][/b]
|
|
func create_request(url: String, method: HTTPClient.Method = HTTPClient.Method.METHOD_GET, headers: PackedStringArray = PackedStringArray([]), body: String = "") -> int:
|
|
logger.verb("Creating new request\n-> URL: " + url + "\n-> Method: " + str(method) + "\nHeaders: " + str(headers.size()) + "\nBody size: " + str(body.length()) + " Characters")
|
|
var id = generate_id()
|
|
list_queue.merge({ id: { "url": url, "method": method, "headers": headers, "body": body } })
|
|
return id
|
|
|
|
## Configures and starts a queued request.[br]
|
|
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b][br]
|
|
## [b]Warning: [i]You'll probably not need this. Only use this function when implementing your own downloading method.[/i][/b]
|
|
func start_request(id: int, parse_utf8: bool) -> void:
|
|
logger.verb("Starting request " + str(id))
|
|
logger.verb(core.stringify_variables("Starting request %id%", { "id": id }))
|
|
list_active.merge({ id: list_queue[id] })
|
|
list_queue.erase(id)
|
|
logger.diag("Creating new HTTPRequest \"Request #" + str(id) + "\"")
|
|
var download: HTTPRequest = HTTPRequest.new()
|
|
download.name = "Request #" + str(id)
|
|
download.accept_gzip = true
|
|
download.use_threads = true
|
|
var lambda: Callable = func(result: int, http_code: int, headers: PackedStringArray, body: PackedByteArray, httprequest: HTTPRequest) -> void:
|
|
logger.verb(core.stringify_variables("Request %id% completed\nResult: %result%\nHTTP response code: %http_code%\nHeaders: %headers%\nBody size: %body_size_bytes% Bytes // %body_size_mib% MiB\nParsed body as UTF-8: %parse_utf8%", { "result": result, "http_code": http_code, "headers": headers.size(), "body_size_bytes": body.size(), "body_size_mib": core.misc.byte2mib(body.size(), true), "parse_utf8": parse_utf8 }, true))
|
|
list_complete.merge({ id: { "result": result, "http_code": http_code, "headers": headers, "body": body, "body_utf8": body.get_string_from_utf8() if parse_utf8 else "" } })
|
|
list_active.erase(id)
|
|
remove_child(httprequest)
|
|
httprequest.queue_free()
|
|
download.connect("request_completed", lambda.bind(download))
|
|
add_child(download)
|
|
download.request(list_active[id]["url"], list_active[id]["headers"], list_active[id]["method"], list_active[id]["body"])
|
|
|
|
## Checks if [code]url[/code] can be used.
|
|
func is_url_allowed(url: String) -> bool:
|
|
if url.begins_with("http://"):
|
|
match(config_unsecure_requests):
|
|
CoreTypes.BlockadeLevel.BLOCK:
|
|
logger.error(core.stringify_variables("Blocked unsecure url %url%", { "url": url }))
|
|
return false
|
|
CoreTypes.BlockadeLevel.WARN: logger.warn(core.stringify_variables("Requesting unsecure url %url%", { "url": url }))
|
|
CoreTypes.BlockadeLevel.IGNORE: pass
|
|
_: await logger.crash(core.stringify_variables("Invalid BlockadeLevel %level%", { "level": config_unsecure_requests }))
|
|
elif url.begins_with("https://"): pass
|
|
else:
|
|
logger.error(core.stringify_variables("Invalid url %url%", { "url": url }))
|
|
return false
|
|
return true
|
|
|
|
## Returns if a request has completed yet.
|
|
func is_request_completed(id: int) -> bool: return list_complete.has(id)
|
|
|
|
## Cleans the request queue.
|
|
func clean_queue() -> void:
|
|
logger.verb("Cleaning request queue")
|
|
list_queue.clear()
|
|
|
|
## Cleans the completed requests list.
|
|
func clean_completed() -> void:
|
|
logger.verb("Cleaning completed requests")
|
|
list_complete.clear()
|