Initial Commit
Initial commit of Code Base.
This commit is contained in:
parent
293b1213e1
commit
c11a4ebbc2
653 changed files with 36893 additions and 1 deletions
111
addons/twitcher/lib/http/buffered-http-icon.svg
Normal file
111
addons/twitcher/lib/http/buffered-http-icon.svg
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg4"
|
||||
sodipodi:docname="buffered-http-icon.svg"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="0.0625"
|
||||
y="10.625"
|
||||
width="17.5"
|
||||
height="10"
|
||||
id="rect30" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect5"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,1,1,0,0.35632304,0,1 @ F,0,1,1,0,0.35632304,0,1 @ F,0,1,1,0,0.35632304,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,0.35632304,0,1 @ F,0,1,1,0,0.35632304,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35632304,0,1 | F,0,0,1,0,0.32388529,0,1 @ F,0,0,1,0,0.30607722,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.21339985,0,1 @ F,0,0,1,0,0.41714928,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect4"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.59205278,0,1 @ F,0,0,1,0,0.62915907,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="45.254834"
|
||||
inkscape:cx="8.6068153"
|
||||
inkscape:cy="15.733126"
|
||||
inkscape:window-width="1999"
|
||||
inkscape:window-height="1360"
|
||||
inkscape:window-x="712"
|
||||
inkscape:window-y="1440"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<rect
|
||||
style="fill:#e0e0e0;fill-opacity:1;stroke:#e0e0e0;stroke-width:1.10201;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect29"
|
||||
width="6.1411905"
|
||||
height="6.1411905"
|
||||
x="6.0629044"
|
||||
y="-2.3126719"
|
||||
transform="matrix(0.92518611,0.37951371,-0.92518611,0.37951371,0,0)" />
|
||||
<rect
|
||||
style="fill:#e0e0e0;fill-opacity:1;stroke:#e0e0e0;stroke-width:1.10201;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect29-2"
|
||||
width="6.1411905"
|
||||
height="6.1411905"
|
||||
x="7.9945164"
|
||||
y="-0.38105631"
|
||||
transform="matrix(0.92518611,0.37951371,-0.92518611,0.37951371,0,0)" />
|
||||
<rect
|
||||
style="fill:#e0e0e0;fill-opacity:1;stroke:#e0e0e0;stroke-width:1.10201;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect29-7"
|
||||
width="6.1411905"
|
||||
height="6.1411905"
|
||||
x="9.9507313"
|
||||
y="1.5751607"
|
||||
transform="matrix(0.92518611,0.37951371,-0.92518611,0.37951371,0,0)" />
|
||||
<path
|
||||
style="fill:#9dff70;fill-opacity:1;stroke:none;stroke-width:1.36221;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 3.4588334,2.784233 C 3.6019977,3.1259911 5.9999986,8.4422283 5.9999986,8.4422283 L 8.5411634,2.784233"
|
||||
id="path29"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:#ff7070;fill-opacity:1;stroke:none;stroke-width:1.36221;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 7.4588334,8.4422283 C 7.6019977,8.1004702 9.9999986,2.784233 9.9999986,2.784233 l 2.5411684,5.6579953"
|
||||
id="path29-8"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
d="m 1,10 v 2 1 2 h 1 v -2 h 1 v 2 H 4 V 10 H 3 v 2 H 2 v -2 z m 4,0 v 1 h 1 v 4 h 1 v -4 h 1 v -1 z m 4,0 v 1 h 1 v 4 h 1 v -4 h 1 v -1 z m 4,0 v 2 1 2 h 1 v -2 h 1 1 v -1 -2 h -2 z m 1,1 h 1 v 1 h -1 z"
|
||||
style="fill:#e0e0e0;fill-opacity:0.99608"
|
||||
id="path1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
37
addons/twitcher/lib/http/buffered-http-icon.svg.import
Normal file
37
addons/twitcher/lib/http/buffered-http-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://517onfw47ulp"
|
||||
path="res://.godot/imported/buffered-http-icon.svg-338b6a52134b1dc8e217747ec727a304.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/lib/http/buffered-http-icon.svg"
|
||||
dest_files=["res://.godot/imported/buffered-http-icon.svg-338b6a52134b1dc8e217747ec727a304.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
204
addons/twitcher/lib/http/buffered_http_client.gd
Normal file
204
addons/twitcher/lib/http/buffered_http_client.gd
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
@icon("./buffered-http-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
## Http client that bufferes the requests and sends them sequentialy
|
||||
class_name BufferedHTTPClient
|
||||
|
||||
|
||||
## Will be send when a new request was added to queue
|
||||
signal request_added(request: RequestData)
|
||||
|
||||
## Will be send when a request is done.
|
||||
signal request_done(response: ResponseData)
|
||||
|
||||
|
||||
## Contains the request data to be send
|
||||
class RequestData extends RefCounted:
|
||||
## The client that the request belongs too
|
||||
var client: BufferedHTTPClient
|
||||
## The request node that is executing the request
|
||||
var http_request: HTTPRequest
|
||||
## Path of the request
|
||||
var path: String
|
||||
## The method that is used to call request
|
||||
var method: int
|
||||
## The request headers
|
||||
var headers: Dictionary
|
||||
## The body that is requested (TODO does it make more sense to make a Byte Array out of it?)
|
||||
var body: String = ""
|
||||
## Amount of retries
|
||||
var retry: int
|
||||
|
||||
## When you are done free the request
|
||||
func queue_free() -> void:
|
||||
http_request.queue_free()
|
||||
|
||||
|
||||
## Contains the response data
|
||||
class ResponseData extends RefCounted:
|
||||
## Result of the request see `HTTPRequest.Result`
|
||||
var result: int
|
||||
## Response code from the request like 200 for OK
|
||||
var response_code: int
|
||||
## the initial request data
|
||||
var request_data: RequestData
|
||||
## The body of the response as byte array
|
||||
var response_data: PackedByteArray
|
||||
## The response header as dictionary, where multiple keys are concatenated with ';'
|
||||
var response_header: Dictionary
|
||||
## Had the response an error
|
||||
var error: bool
|
||||
|
||||
## When you are done free the request
|
||||
func queue_free() -> void:
|
||||
request_data.queue_free()
|
||||
|
||||
## When a request fails max_error_count then cancel that request -1 for endless amount of tries.
|
||||
@export var max_error_count : int = -1
|
||||
@export var custom_header : Dictionary[String, String] = { "Accept": "*/*" }
|
||||
|
||||
var requests : Array[RequestData] = []
|
||||
var current_request : RequestData
|
||||
var current_response_data : PackedByteArray = PackedByteArray()
|
||||
var responses : Dictionary = {}
|
||||
var error_count : int
|
||||
|
||||
|
||||
## Only one poll at a time so block for all other tries to call it
|
||||
var polling: bool
|
||||
var processing: bool:
|
||||
get: return not requests.is_empty() || current_request != null
|
||||
|
||||
|
||||
## Starts a request that will be handled as soon as the client gets free.
|
||||
## Use HTTPClient.METHOD_* for the method.
|
||||
func request(path: String, method: int, headers: Dictionary, body: String) -> RequestData:
|
||||
logInfo("[%s] start request " % [ path ])
|
||||
headers = headers.duplicate()
|
||||
headers.merge(custom_header)
|
||||
var req = RequestData.new()
|
||||
req.path = path
|
||||
req.method = method
|
||||
req.body = body
|
||||
req.headers = headers
|
||||
req.client = self
|
||||
req.http_request = HTTPRequest.new()
|
||||
req.http_request.use_threads = true
|
||||
req.http_request.timeout = 30
|
||||
req.http_request.request_completed.connect(_on_request_completed.bind(req))
|
||||
add_child(req.http_request)
|
||||
var err : Error = req.http_request.request(req.path, _pack_headers(req.headers), req.method, req.body)
|
||||
if err != OK: logError("Problems with request to %s cause of %s" % [path, error_string(err)])
|
||||
requests.append(req)
|
||||
request_added.emit(req)
|
||||
logDebug("[%s] request started " % [ path ])
|
||||
return req
|
||||
|
||||
|
||||
## When the response is available return it otherwise wait for the response
|
||||
func wait_for_request(request_data: RequestData) -> ResponseData:
|
||||
if responses.has(request_data):
|
||||
var response = responses[request_data]
|
||||
responses.erase(request_data)
|
||||
request_data.queue_free()
|
||||
logDebug("response cached return directly from wait")
|
||||
return response
|
||||
|
||||
var latest_response : ResponseData = null
|
||||
while (latest_response == null || request_data != latest_response.request_data):
|
||||
latest_response = await request_done
|
||||
logDebug("response received return from wait")
|
||||
responses.erase(request_data)
|
||||
request_data.queue_free()
|
||||
return latest_response
|
||||
|
||||
|
||||
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, request_data: RequestData) -> void:
|
||||
var response_data : ResponseData = ResponseData.new()
|
||||
if result != HTTPRequest.Result.RESULT_SUCCESS:
|
||||
logInfo("[%s] problems with result \n\t> response code: %s \n\t> body: %s" % [request_data.path, response_code, body.get_string_from_utf8()])
|
||||
response_data.error = true
|
||||
if result == HTTPRequest.Result.RESULT_CONNECTION_ERROR || result == HTTPRequest.Result.RESULT_TLS_HANDSHAKE_ERROR:
|
||||
if request_data.retry == max_error_count:
|
||||
printerr("Maximum amount of retries for the request. Abort request: %s" % [request_data.path])
|
||||
return
|
||||
var wait_time = pow(2, request_data.retry)
|
||||
wait_time = min(wait_time, 30)
|
||||
logDebug("Error happend during connection. Wait for %s" % wait_time)
|
||||
await get_tree().create_timer(wait_time, true, false, true).timeout
|
||||
var http_request: HTTPRequest = request_data.http_request.duplicate()
|
||||
add_child(http_request)
|
||||
request_data.http_request = http_request
|
||||
request_data.retry += 1
|
||||
http_request.request(request_data.path, _pack_headers(request_data.headers), request_data.method, request_data.body)
|
||||
http_request.request_completed.connect(_on_request_completed.bind(http_request))
|
||||
|
||||
response_data.result = result
|
||||
response_data.request_data = request_data
|
||||
response_data.response_data = body
|
||||
response_data.response_code = response_code
|
||||
response_data.response_header = _get_response_headers_as_dictionary(headers)
|
||||
responses[request_data] = response_data
|
||||
logInfo("[%s] request done with result HTTPRequest.Result[%s] " % [ request_data.path, result])
|
||||
request_done.emit(response_data)
|
||||
|
||||
|
||||
func _get_response_headers_as_dictionary(headers: PackedStringArray) -> Dictionary:
|
||||
var header_dict: Dictionary = {}
|
||||
if headers == null:
|
||||
return header_dict
|
||||
|
||||
for header in headers:
|
||||
var header_data = header.split(":", true, 1)
|
||||
var key = header_data[0]
|
||||
var val = header_data[1]
|
||||
if header_dict.has(key):
|
||||
header_dict[key] += "; " + val
|
||||
else:
|
||||
header_dict[key] = val
|
||||
return header_dict
|
||||
|
||||
|
||||
func _pack_headers(headers: Dictionary) -> PackedStringArray:
|
||||
var result: PackedStringArray = []
|
||||
for header_key in headers:
|
||||
var header_value = headers[header_key]
|
||||
result.append("%s: %s" % [header_key, header_value])
|
||||
return result
|
||||
|
||||
|
||||
## The amount of requests that are pending
|
||||
func queued_request_size() -> int:
|
||||
var requests_size: int = requests.size()
|
||||
if current_request != null:
|
||||
requests_size += 1
|
||||
return requests_size
|
||||
|
||||
|
||||
func empty_response(request_data: RequestData) -> ResponseData:
|
||||
var response_data = ResponseData.new()
|
||||
response_data.request_data = request_data
|
||||
response_data.response_data = []
|
||||
response_data.response_code = 0
|
||||
response_data.response_header = {}
|
||||
response_data.result = 0
|
||||
return response_data
|
||||
|
||||
|
||||
# === LOGGER ===
|
||||
|
||||
static var logger: Dictionary = {}
|
||||
static func set_logger(error: Callable, info: Callable, debug: Callable) -> void:
|
||||
logger.debug = debug
|
||||
logger.info = info
|
||||
logger.error = error
|
||||
|
||||
static func logDebug(text: String) -> void:
|
||||
if logger.has("debug"): logger.debug.call(text)
|
||||
|
||||
static func logInfo(text: String) -> void:
|
||||
if logger.has("info"): logger.info.call(text)
|
||||
|
||||
static func logError(text: String) -> void:
|
||||
if logger.has("error"): logger.error.call(text)
|
||||
1
addons/twitcher/lib/http/buffered_http_client.gd.uid
Normal file
1
addons/twitcher/lib/http/buffered_http_client.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b7i5j62lmuh71
|
||||
58
addons/twitcher/lib/http/debug_buffered_http_client.gd
Normal file
58
addons/twitcher/lib/http/debug_buffered_http_client.gd
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
extends Control
|
||||
|
||||
@onready var clients: Tree = %Clients
|
||||
|
||||
## Key: BufferedHTTPClient | value: TreeItem
|
||||
var client_map : Dictionary[BufferedHTTPClient, TreeItem] = {}
|
||||
## Key: RequestData | value: TreeItem
|
||||
var request_map : Dictionary[BufferedHTTPClient.RequestData, TreeItem] = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
get_tree().root.child_entered_tree.connect(_on_child_enter)
|
||||
_add_http_clients(get_tree().root)
|
||||
|
||||
|
||||
func _add_http_clients(parent: Node) -> void:
|
||||
for child in parent.get_children():
|
||||
_on_child_enter(child)
|
||||
_add_http_clients(child)
|
||||
|
||||
|
||||
func _on_child_enter(node: Node) -> void:
|
||||
if node is BufferedHTTPClient:
|
||||
_new_client(node)
|
||||
|
||||
|
||||
func _new_client(client: BufferedHTTPClient):
|
||||
var parent = clients.create_item()
|
||||
parent.set_text(0, client.name)
|
||||
|
||||
client_map[client] = parent
|
||||
|
||||
client.request_added.connect(_on_add_request.bind(parent))
|
||||
client.request_done.connect(_on_done_request)
|
||||
for request in client.requests:
|
||||
_on_add_request(request, parent)
|
||||
|
||||
|
||||
func _on_add_request(request: BufferedHTTPClient.RequestData, http_item: TreeItem):
|
||||
var request_item = clients.create_item(http_item)
|
||||
request_item.set_text(0, request.path)
|
||||
request_item.set_text(1, "Queued")
|
||||
request_map[request] = request_item
|
||||
|
||||
|
||||
func _on_done_request(response: BufferedHTTPClient.ResponseData):
|
||||
var request_item = request_map[response.request_data] as TreeItem
|
||||
request_item.set_text(1, "DONE")
|
||||
await get_tree().create_timer(60, true, false, true).timeout
|
||||
if request_item != null: request_item.free()
|
||||
|
||||
|
||||
func _close_client(client: BufferedHTTPClient):
|
||||
var http_item = client_map[client] as TreeItem
|
||||
client_map.erase(client)
|
||||
http_item.set_text(1, "CLOSED")
|
||||
await get_tree().create_timer(60, true, false, true).timeout
|
||||
http_item.free()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bpvg80y7vsysx
|
||||
25
addons/twitcher/lib/http/debug_buffered_http_client.tscn
Normal file
25
addons/twitcher/lib/http/debug_buffered_http_client.tscn
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://b0ebhuv2yaow0"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bpvg80y7vsysx" path="res://addons/twitcher/lib/http/debug_buffered_http_client.gd" id="1_ij68s"]
|
||||
|
||||
[node name="DebugBufferedHttpClient" type="VBoxContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_ij68s")
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
text = "Debug HTTP Clients"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Clients" type="Tree" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
columns = 2
|
||||
hide_root = true
|
||||
177
addons/twitcher/lib/http/http_server.gd
Normal file
177
addons/twitcher/lib/http/http_server.gd
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
@tool
|
||||
extends Node
|
||||
|
||||
## Provides a simple HTTP Service to serve web stuff
|
||||
class_name HTTPServer
|
||||
|
||||
## Key: int | Value: WeakRef(Server)
|
||||
static var _servers : Dictionary = {}
|
||||
|
||||
|
||||
class Server extends TCPServer:
|
||||
var _bind_address: String
|
||||
var _port: int
|
||||
var _clients: Array[Client] = []
|
||||
var _listeners: int
|
||||
|
||||
signal request_received(client: Client)
|
||||
signal client_connected(client: Client)
|
||||
signal client_disconnected(client: Client)
|
||||
signal client_error_occured(client: Client, error: Error)
|
||||
|
||||
|
||||
func _init(bind_address: String, port: int) -> void:
|
||||
_bind_address = bind_address
|
||||
_port = port
|
||||
HTTPServer._servers[_port] = weakref(self)
|
||||
|
||||
|
||||
func start_listening() -> void:
|
||||
_listeners += 1
|
||||
if !is_listening():
|
||||
var status: Error = listen(_port, _bind_address)
|
||||
Engine.get_main_loop().process_frame.connect(_process)
|
||||
if status != OK:
|
||||
HTTPServer.logError("Could not listen to port %d: %s" % [_port, error_string(status)])
|
||||
else:
|
||||
HTTPServer.logInfo("{%s:%s} listening" % [ _bind_address, _port ])
|
||||
else:
|
||||
HTTPServer.logDebug("{%s:%s} already listening" % [ _bind_address, _port ])
|
||||
|
||||
|
||||
func stop_listening() -> void:
|
||||
_listeners -= 1
|
||||
HTTPServer.logDebug("{%s:%s} listener node detached %s left" % [ _bind_address, _port, _listeners ])
|
||||
if _listeners <= 0:
|
||||
_stop_server()
|
||||
|
||||
|
||||
func _stop_server() -> void:
|
||||
HTTPServer.logInfo("{%s:%s} stop" % [ _bind_address, _port ])
|
||||
Engine.get_main_loop().process_frame.disconnect(_process)
|
||||
for client in _clients:
|
||||
client.peer.disconnect_from_host()
|
||||
stop()
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
HTTPServer.logInfo("{%s:%s} removed" % [ _bind_address, _port ])
|
||||
HTTPServer._servers.erase(_port)
|
||||
|
||||
|
||||
func _process() -> void:
|
||||
if !is_listening(): return
|
||||
|
||||
if is_connection_available():
|
||||
_handle_connect()
|
||||
|
||||
for client in _clients:
|
||||
_process_request(client)
|
||||
_handle_disconnect(client)
|
||||
|
||||
|
||||
func _process_request(client: Client) -> void:
|
||||
var peer := client.peer
|
||||
if peer.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
||||
var error = peer.poll()
|
||||
if error != OK:
|
||||
HTTPServer.logError("Could not poll client %d: %s" % [_port, error_string(error)])
|
||||
client_error_occured.emit(client, error)
|
||||
elif peer.get_available_bytes() > 0:
|
||||
request_received.emit(client)
|
||||
|
||||
|
||||
func _handle_connect() -> void:
|
||||
var peer := take_connection()
|
||||
var client := Client.new()
|
||||
client.peer = peer
|
||||
_clients.append(client)
|
||||
client_connected.emit(client)
|
||||
HTTPServer.logInfo("{%s:%s} client connected" % [ _bind_address, _port ])
|
||||
|
||||
|
||||
func _handle_disconnect(client: Client) -> void:
|
||||
if client.peer.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
||||
client_disconnected.emit(client)
|
||||
HTTPServer.logInfo("{%s:%s} client disconnected" % [ _bind_address, _port ])
|
||||
_clients.erase(client)
|
||||
|
||||
|
||||
class Client extends RefCounted:
|
||||
var peer: StreamPeerTCP
|
||||
|
||||
|
||||
## Called when a new request was made
|
||||
signal request_received(client: Client)
|
||||
|
||||
|
||||
@export var _port: int
|
||||
@export var _bind_address: String
|
||||
|
||||
var _server : Server
|
||||
var _listening: bool
|
||||
|
||||
|
||||
static func create(port: int, bind_address: String = "*") -> HTTPServer:
|
||||
var server = HTTPServer.new()
|
||||
server._bind_address = bind_address
|
||||
server._port = port
|
||||
return server
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if _servers.has(_port) && _servers[_port] != null:
|
||||
_server = _servers[_port].get_ref()
|
||||
else:
|
||||
_server = Server.new(_bind_address, _port)
|
||||
_server.request_received.connect(_on_request_received)
|
||||
logInfo("{%s:%s} start" % [ _bind_address, _port ])
|
||||
|
||||
|
||||
func _on_request_received(client: Client) -> void:
|
||||
request_received.emit(client)
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
stop_listening()
|
||||
|
||||
|
||||
func start_listening() -> void:
|
||||
_listening = true
|
||||
_server.start_listening()
|
||||
|
||||
|
||||
func stop_listening() -> void:
|
||||
if _listening:
|
||||
_listening = false
|
||||
_server.stop_listening()
|
||||
|
||||
|
||||
func send_response(client: Client, response_code : String, body : PackedByteArray) -> void:
|
||||
var peer = client.peer
|
||||
peer.put_data(("HTTP/1.1 %s\r\n" % response_code).to_utf8_buffer())
|
||||
peer.put_data("Server: Godot Engine (Twitcher)\r\n".to_utf8_buffer())
|
||||
peer.put_data(("Content-Length: %d\r\n"% body.size()).to_utf8_buffer())
|
||||
peer.put_data("Connection: close\r\n".to_utf8_buffer())
|
||||
peer.put_data("Content-Type: text/html charset=UTF-8\r\n".to_utf8_buffer())
|
||||
peer.put_data("\r\n".to_utf8_buffer())
|
||||
peer.put_data(body)
|
||||
|
||||
|
||||
# === LOGGER ===
|
||||
static var logger: Dictionary = {}
|
||||
static func set_logger(error: Callable, info: Callable, debug: Callable) -> void:
|
||||
logger.debug = debug
|
||||
logger.info = info
|
||||
logger.error = error
|
||||
|
||||
static func logDebug(text: String) -> void:
|
||||
if logger.has("debug"): logger.debug.call(text)
|
||||
|
||||
static func logInfo(text: String) -> void:
|
||||
if logger.has("info"): logger.info.call(text)
|
||||
|
||||
static func logError(text: String) -> void:
|
||||
if logger.has("error"): logger.error.call(text)
|
||||
1
addons/twitcher/lib/http/http_server.gd.uid
Normal file
1
addons/twitcher/lib/http/http_server.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bnepo370sikkb
|
||||
25
addons/twitcher/lib/http/http_util.gd
Normal file
25
addons/twitcher/lib/http/http_util.gd
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
extends Object
|
||||
|
||||
## Parses a query string and returns a dictionary with the parameters.
|
||||
static func parse_query(query: String) -> Dictionary:
|
||||
var parameters = Dictionary()
|
||||
# Split the query by '&' to separate different parameters.
|
||||
var pairs = query.split("&")
|
||||
# Iterate over each pair of key-value.
|
||||
for pair in pairs:
|
||||
# Split the pair by '=' to separate the key from the value.
|
||||
var kv = pair.split("=")
|
||||
if kv.size() == 2:
|
||||
var key = kv[0].strip_edges()
|
||||
var value = kv[1].strip_edges()
|
||||
var decoded_key = key.uri_decode()
|
||||
var decoded_value = value.uri_decode()
|
||||
parameters[decoded_key] = decoded_value
|
||||
return parameters
|
||||
|
||||
|
||||
## Method to set all logger within this package
|
||||
static func set_logger(error: Callable, info: Callable, debug: Callable) -> void:
|
||||
BufferedHTTPClient.set_logger(error, info, debug)
|
||||
HTTPServer.set_logger(error, info, debug)
|
||||
WebsocketClient.set_logger(error, info, debug)
|
||||
1
addons/twitcher/lib/http/http_util.gd.uid
Normal file
1
addons/twitcher/lib/http/http_util.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://5esrbr8ikth8
|
||||
145
addons/twitcher/lib/http/websocket_client.gd
Normal file
145
addons/twitcher/lib/http/websocket_client.gd
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
@tool
|
||||
extends Node
|
||||
|
||||
## Advanced websocket client that automatically reconnects to the server
|
||||
class_name WebsocketClient
|
||||
|
||||
|
||||
## Called as soon the websocket got a connection
|
||||
signal connection_established
|
||||
|
||||
## Called as soon the websocket closed the connection
|
||||
signal connection_closed
|
||||
|
||||
## Called when a complete message got received
|
||||
signal message_received(message: PackedByteArray)
|
||||
|
||||
## Called when the state of the websocket changed
|
||||
signal connection_state_changed(state : WebSocketPeer.State)
|
||||
|
||||
@export var connection_url: String:
|
||||
set(val):
|
||||
_logDebug("Set connection to %s" % val)
|
||||
connection_url = val
|
||||
|
||||
var connection_state : WebSocketPeer.State = WebSocketPeer.STATE_CLOSED:
|
||||
set(new_state):
|
||||
if new_state != connection_state:
|
||||
connection_state_changed.emit(new_state)
|
||||
if new_state == WebSocketPeer.STATE_OPEN: connection_established.emit()
|
||||
if new_state == WebSocketPeer.STATE_CLOSED: connection_closed.emit()
|
||||
connection_state = new_state
|
||||
|
||||
## Determines if a connection should be established or not
|
||||
@export var auto_reconnect: bool:
|
||||
set(val):
|
||||
auto_reconnect = val
|
||||
_logDebug("New auto_reconnect value: %s" % val)
|
||||
|
||||
## True if currently connecting to prevent 2 connectionen processes at the same time
|
||||
var _is_already_connecting: bool
|
||||
var is_open: bool:
|
||||
get(): return _peer.get_ready_state() == WebSocketPeer.STATE_OPEN
|
||||
var is_closed: bool:
|
||||
get(): return _peer.get_ready_state() == WebSocketPeer.STATE_CLOSED
|
||||
|
||||
var _peer: WebSocketPeer = WebSocketPeer.new()
|
||||
var _tries: int
|
||||
|
||||
|
||||
func open_connection() -> void:
|
||||
if not is_closed: return
|
||||
auto_reconnect = true
|
||||
_logInfo("Open connection")
|
||||
await _establish_connection()
|
||||
|
||||
|
||||
func wait_connection_established() -> void:
|
||||
if is_open: return
|
||||
await connection_established
|
||||
|
||||
func _establish_connection() -> void:
|
||||
if _is_already_connecting || not is_closed: return
|
||||
_is_already_connecting = true
|
||||
var wait_time = pow(2, _tries)
|
||||
_logDebug("Wait %s before connecting" % [wait_time])
|
||||
await get_tree().create_timer(wait_time, true, false, true).timeout
|
||||
_logInfo("Connecting to %s" % connection_url)
|
||||
var err = _peer.connect_to_url(connection_url)
|
||||
if err != OK:
|
||||
logError("Couldn't connect cause of %s" % [error_string(err)])
|
||||
_tries += 1
|
||||
_is_already_connecting = false
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if auto_reconnect: open_connection()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if not is_open: return
|
||||
_peer.close(1000, "resource got freed")
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_poll()
|
||||
|
||||
|
||||
func _poll() -> void:
|
||||
if connection_url == "": return
|
||||
|
||||
var state := _peer.get_ready_state()
|
||||
if state == WebSocketPeer.STATE_CLOSED and auto_reconnect:
|
||||
_establish_connection()
|
||||
_peer.poll()
|
||||
_handle_state_changes(state)
|
||||
connection_state = state
|
||||
if state == WebSocketPeer.STATE_OPEN:
|
||||
_read_data()
|
||||
|
||||
|
||||
func _handle_state_changes(state: WebSocketPeer.State) -> void:
|
||||
if connection_state != WebSocketPeer.STATE_OPEN && state == WebSocketPeer.STATE_OPEN:
|
||||
_logInfo("connected")
|
||||
_tries = 0
|
||||
|
||||
if connection_state != WebSocketPeer.STATE_CLOSED && state == WebSocketPeer.STATE_CLOSED:
|
||||
_logInfo("connection was closed [%s]: %s" % [_peer.get_close_code(), _peer.get_close_reason()])
|
||||
|
||||
|
||||
func _read_data() -> void:
|
||||
while (_peer.get_available_packet_count()):
|
||||
message_received.emit(_peer.get_packet())
|
||||
|
||||
|
||||
func send_text(message: String) -> Error:
|
||||
return _peer.send_text(message)
|
||||
|
||||
|
||||
func close(status: int = 1000, message: String = "Normal Closure") -> void:
|
||||
_logDebug("Websocket activly closed")
|
||||
auto_reconnect = false
|
||||
_peer.close(status, message)
|
||||
|
||||
|
||||
# === LOGGER ===
|
||||
static var logger: Dictionary = {}
|
||||
static func set_logger(error: Callable, info: Callable, debug: Callable) -> void:
|
||||
logger.debug = debug
|
||||
logger.info = info
|
||||
logger.error = error
|
||||
|
||||
func _logDebug(text: String) -> void:
|
||||
logDebug("[%s]: %s" % [connection_url, text])
|
||||
|
||||
static func logDebug(text: String) -> void:
|
||||
if logger.has("debug"): logger.debug.call(text)
|
||||
|
||||
func _logInfo(text: String) -> void:
|
||||
logInfo("[%s]: %s" % [connection_url, text])
|
||||
|
||||
static func logInfo(text: String) -> void:
|
||||
if logger.has("info"): logger.info.call(text)
|
||||
|
||||
static func logError(text: String) -> void:
|
||||
if logger.has("error"): logger.error.call(text)
|
||||
1
addons/twitcher/lib/http/websocket_client.gd.uid
Normal file
1
addons/twitcher/lib/http/websocket_client.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://buqbforpa7b8a
|
||||
Loading…
Add table
Add a link
Reference in a new issue