diff --git a/addons/sammi/lib/sammi_api.gd b/addons/sammi/lib/sammi_api.gd new file mode 100644 index 0000000..7e3f008 --- /dev/null +++ b/addons/sammi/lib/sammi_api.gd @@ -0,0 +1,408 @@ +extends Node +class_name SammiAPI + +signal request_completed(response: ApiResponse) + +@export_category("SAMMI Connection Information") +@export var host: String = "127.0.0.1" +@export var port: int = 9450 +@export var password: String = "" + +var client: SammiClient +var last_error: String = "" +var last_error_description: String = "" + +func _ready() -> void: + client = SammiClient.new() + client.host = host + client.port = port + client.password = password + client.request_completed.connect(_on_request_completed) + +func get_variable(name: String, button_id: String = "") -> Variant: + var api_path: String = "/api?request=getVariable&name=%s" % name + if button_id != "": + api_path += "&button_id=%s" % button_id + client.request(SammiClient.Method.GET, api_path, "") + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.contains("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return res + else: + return res["data"] + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return null + +func set_variable(name: String, value: Variant, button_id) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "setVariable", + "name": name, + "value": value, + "button_id": button_id + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: Invalid JSON Returned. Body: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func delete_variable(name: String, button_id) -> bool: + var api_path: String = "/api" % name + var req: Dictionary = { + "request": "deleteVariable", + "name": name, + "button_id": button_id + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func insert_array(name: String, position: int, value: Variant, button_id: String) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "insertArray", + "name": name, + "position": position, + "value": value, + "button_id": button_id + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func delete_array(name: String, position: int, button_id: String) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "deleteArray", + "name": name, + "position": position, + "button_id": button_id + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func get_deck_status(deck_id: String) -> bool: + var api_path: String = "/api?request=getDeckStatus&deck_id=%s" % deck_id + client.request(SammiClient.Method.GET, api_path, "") + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + return true + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func change_deck_status(deck_id: String, status: int) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "changeDeckStatus", + "deck_id": deck_id, + "status": status + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func trigger_button(button_id: String) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "triggerButton", + "button_id": button_id + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func release_button(button_id: String) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "releaseButton", + "button_id": button_id + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func modify_button(button_id: String, text: String = "", color: int = -1, image: String = "", border: int = -1) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "modifyButton", + "button_id": button_id, + } + if text != "": + req["text"] = text + if color != -1: + req["color"] = color + if image != "": + req["image"] = image + if border != -1: + req["border"] = border + + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func popup_message(message: String) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "popupMessage", + "message": message + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func alert_message(message: String) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "alertMessage", + "message": message + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + +func notification_message(message: String, title: String) -> bool: + var api_path: String = "/api" + var req: Dictionary = { + "request": "notificationMessage", + "message": message, + "title": title + } + client.request(SammiClient.Method.POST, api_path, JSON.stringify(req)) + var response: SammiClient.SammiResponse = await client.request_completed + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("data") and res["data"] == "Ok.": + return true + elif res.has("error"): + push_error("Error: %s: %s" % [res["Error"], res["Description"]]) + last_error = res["Error"] + last_error_description = res["Description"] + return false + else: + push_error("Error: %s" % response.body) + last_error = "Invalid JSON Returned" + last_error_description = response.body + return false + else: + push_error("Error: %d: %s" % [response.code, response.error_message]) + last_error = "HTTP Error: %d" % response.code + last_error_description = response.error_message + return false + + +func _on_request_completed(response: SammiClient.SammiResponse) -> void: + var resp: ApiResponse = ApiResponse.new() + if response.code == 200: + var res := JSON.parse_string(response.body) + if res.has("error"): + resp.error = res["Error"] + resp.error_description = res["Description"] + else: + resp.data = res["data"] + else: + resp.error = "HTTP Error: %d" % response.code + resp.error_description = response.error_message + request_completed.emit(resp) + + +class ApiResponse: + var data: Variant + var error: String + var error_description: String diff --git a/addons/sammi/lib/sammi_api.gd.uid b/addons/sammi/lib/sammi_api.gd.uid new file mode 100644 index 0000000..0ae3236 --- /dev/null +++ b/addons/sammi/lib/sammi_api.gd.uid @@ -0,0 +1 @@ +uid://btwsi3vi072ku diff --git a/addons/sammi/lib/sammi_client.gd b/addons/sammi/lib/sammi_client.gd new file mode 100644 index 0000000..eb96d35 --- /dev/null +++ b/addons/sammi/lib/sammi_client.gd @@ -0,0 +1,162 @@ +extends Node +class_name SammiClient + +## Creates a HTTP Client connection to communicate with SAMMI. Due to current restrictions +## Godot expects Content-Length or transfer-enoding: chunked headers to be set properly, otherwise +## Godot assumes there is no body, and doesn't read it. SAMMI's custom internal HTTP Server does +## not set the Content-Length header for response headers to the API. Hence it will fail with a +## normal Godot HTTPClient. + +signal request_completed(response: SammiResponse) + +@export_category("SAMMI Connection Information") +@export var host: String = "127.0.0.1" +@export var port: int = 9450 +@export var user_agent: String = "GodotSammi/0.1 (%s)" % OS.get_name() +@export_category("Authentication") +@export var password: String = "" + +var client: StreamPeerTCP +var state: State = State.CLOSED +var buffer: PackedByteArray +var headers: PackedStringArray +var response: SammiResponse +var query: String = "" +var method: Method = Method.GET +var body: String = "" + +enum State { + CONNECTING, + REQUESTING, + RESPONSE, + ERROR, + CLOSED +} + +enum Method { + GET, + POST, +} + +func request(method: Method, api_path: String, body: String) -> void: + assert(state == State.CLOSED, "A request is in the process of being made.") + + self.query = api_path + self.body = body + self.client = StreamPeerTCP.new() + self.client.connect_to_host(host, port) + self.state = State.CONNECTING + +func _process(_delta: float) -> void: + if not client: + return + + if state == State.CLOSED: + client.disconnect_from_host() + client = null + return + + client.poll() + if client.get_status() == StreamPeerTCP.Status.STATUS_CONNECTED and state == State.CONNECTING: + state = State.REQUESTING + _send_request() + elif client.get_status() == StreamPeerTCP.Status.STATUS_CONNECTED and state == State.REQUESTING: + state = State.RESPONSE + _read_response() + elif client.get_status() == StreamPeerTCP.Status.STATUS_CONNECTED and state == State.RESPONSE: + _read_response() + elif client.get_status() == StreamPeerTCP.Status.STATUS_ERROR: + state = State.ERROR + print("Error: ", client.get_error()) + client.disconnect_from_host() + client = null + +func _send_request() -> void: + var request_str = "" + if method == Method.GET: + request_str = "GET %s HTTP/1.1\r\n" % query + else: + request_str = "POST %s HTTP/1.1\r\n" % query + + + request_str += "Host: %s:%d\r\n" % [host, port] + request_str += "User-Agent: %s\r\n" % user_agent + request_str += "Accept: */*\r\n" + if password != "": + request_str += "Authorization: %s\r\n" % password + if method == Method.POST and body != "": + request_str += "Content-Type: application/json\r\n" + request_str += "Content-Length: %d\r\n" % body.length() + + request_str += "\r\n" + + if method == Method.POST and body != "": + request_str += body + client.put_data(request_str.to_utf8_buffer()) + +func _read_response() -> void: + client.poll() + + if headers.size() == 0: + while true: + var byte := client.get_partial_data(1) + if byte[0] == OK and byte[1].size() == 0: + await get_tree().process_frame + client.poll() + continue + elif byte[0] == OK: + buffer.push_back(byte[1][0]) + else: + state = State.ERROR + print("Error reading response headers.") + return + + if buffer.size() < 4: + await get_tree().process_frame + client.poll() + continue + + if (buffer[-2] == 10 and buffer[-1] == 10) or \ + (buffer[-4] == 13 and buffer[-3] == 10 and buffer[-2] == 13 and buffer[-1] == 10): + var data := buffer.get_string_from_utf8() + buffer = [] + headers = data.replace("\r", "").rstrip("\n\n").split("\n") + break + await get_tree().process_frame + client.poll() + + if headers.size() == 0: + return + + var chunk := client.get_partial_data(1024) + if chunk[0] == OK and chunk[1].size() > 0: + buffer.append_array(chunk[1]) + else: + response = SammiResponse.new(headers, buffer, query) + request_completed.emit(response) + state = State.CLOSED + + +class SammiResponse: + var code: int + var headers: Dictionary + var body: String + var query: String + var error_message: String + + func _init(p_headers: PackedStringArray, p_buffer: PackedByteArray, p_query: String) -> void: + body = p_buffer.get_string_from_utf8() + headers = {} + for line in p_headers: + if line.contains(": "): + var parts := line.split(": ") + headers[parts[0]] = parts[1] + else: + var parts := Array(line.split(" ")) + if parts.pop_front() == "HTTP/1.1": + code = int(parts.pop_front()) + error_message = " ".join(parts) + else: + code = 500 + error_message = "Unknown Error" + query = p_query \ No newline at end of file diff --git a/addons/sammi/lib/sammi_client.gd.uid b/addons/sammi/lib/sammi_client.gd.uid new file mode 100644 index 0000000..9fd6eb7 --- /dev/null +++ b/addons/sammi/lib/sammi_client.gd.uid @@ -0,0 +1 @@ +uid://bwhiwlbct7ijv diff --git a/addons/sammi/plugin.cfg b/addons/sammi/plugin.cfg new file mode 100644 index 0000000..e90dada --- /dev/null +++ b/addons/sammi/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Sammi Plugin" +description="Plugin to connect to the API for Sammi (https://sammi.solutions)" +author="Mario Steele" +version="0.1" +script="plugin.gd" diff --git a/addons/sammi/plugin.gd b/addons/sammi/plugin.gd new file mode 100644 index 0000000..aa1b8e0 --- /dev/null +++ b/addons/sammi/plugin.gd @@ -0,0 +1,12 @@ +@tool +extends EditorPlugin + + +func _enter_tree() -> void: + # Initialization of the plugin goes here. + pass + + +func _exit_tree() -> void: + # Clean-up of the plugin goes here. + pass diff --git a/addons/sammi/plugin.gd.uid b/addons/sammi/plugin.gd.uid new file mode 100644 index 0000000..c276c27 --- /dev/null +++ b/addons/sammi/plugin.gd.uid @@ -0,0 +1 @@ +uid://b0yib03xttmn8 diff --git a/project.godot b/project.godot index 7e5a388..8cf0fce 100644 --- a/project.godot +++ b/project.godot @@ -14,3 +14,7 @@ config/name="HttpClientDebugger" config/description="Enter an interesting project description here!" config/features=PackedStringArray("4.4") config/icon="res://icon.png" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/sammi/plugin.cfg")