From 4eed4393e4aeae64158ca39d3d61a0f3f1b99225 Mon Sep 17 00:00:00 2001 From: Jamie Greunbaum Date: Sun, 2 Jun 2024 03:18:49 -0400 Subject: [PATCH] Bugzilla API is now fully implemented. --- Scripts/server_bugzilla_api.gd | 298 +++++++++++++++++++++++++++++++++ bugbot.gd | 15 +- 2 files changed, 308 insertions(+), 5 deletions(-) diff --git a/Scripts/server_bugzilla_api.gd b/Scripts/server_bugzilla_api.gd index 79ddffb..7dd544f 100644 --- a/Scripts/server_bugzilla_api.gd +++ b/Scripts/server_bugzilla_api.gd @@ -1,6 +1,304 @@ class_name BugbotServerBugzillaAPI extends "res://addons/Bugbot/Scripts/server_api.gd" +##region Consts +const DEFAULT_SERVER : StringName = &"https://bugzilla.example.com" +const DEFAULT_REST_URI : StringName = &"rest/" +const DEFAULT_PRODUCT_NAME : StringName = &"ProductName" +const DEFAULT_API_KEY : StringName = &"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" +const DEFAULT_POST_STATUS : StringName = &"UNCONFIRMED" + +const DEFAULT_MAP_NAME_FIELD : StringName = &"cf_mapname" +const DEFAULT_MARKER_LOCATION_FIELD : StringName = &"cf_location" + +const DEFAULT_UNRESOLVED_STATUSES : Array = [&"UNCONFIRMED", &"CONFIRMED"] +const DEFAULT_IN_PROGRESS_STATUSES : Array = [&"IN_PROGRESS"] +const DEFAULT_RESOLVED_STATUSES : Array = [&"RESOLVED", &"VERIFIED"] +#endregion + + +func __return_list_of_bugs_thread(map_name:String, callback:Callable) -> void: + var http_client : HTTPClient = HTTPClient.new() + if __connect_to_server(http_client, ProjectSettings.get_setting("bugbot/reporting/bugzilla/server", DEFAULT_SERVER)) != HTTPClient.STATUS_CONNECTED: + printerr("Could not connect to server.") + return + + var show_unresolved : bool = ProjectSettings.get_setting("bugbot/markers/unresolved/show_unresolved_bugs", DEFAULT_SHOW_UNRESOLVED_BUGS) + var show_in_progress : bool = ProjectSettings.get_setting("bugbot/markers/in_progress/show_in_progress_bugs", DEFAULT_SHOW_IN_PROGRESS_BUGS) + var show_resolved : bool = ProjectSettings.get_setting("bugbot/markers/resolved/show_resolved_bugs", DEFAULT_SHOW_RESOLVED_BUGS) + var unresolved_labels : Array = ProjectSettings.get_setting("bugbot/reporting/bugzilla/status_labels/unresolved_statuses", DEFAULT_UNRESOLVED_STATUSES) + var in_progress_labels : Array = ProjectSettings.get_setting("bugbot/reporting/bugzilla/status_labels/in_progress_statuses", DEFAULT_IN_PROGRESS_STATUSES) + var resolved_labels : Array = ProjectSettings.get_setting("bugbot/reporting/bugzilla/status_labels/resolved_statuses", DEFAULT_RESOLVED_STATUSES) + + var map_name_field : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/map_name_field", DEFAULT_MAP_NAME_FIELD) + var marker_location_field : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/marker_location_field", DEFAULT_MARKER_LOCATION_FIELD) + var api_key : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/API_key", DEFAULT_API_KEY) + var api_url : String = __build_url_string("bug?") + api_url += map_name_field + "=" + map_name + if show_unresolved: + for unresolved_status in unresolved_labels: + api_url += "&status=" + unresolved_status + if show_in_progress: + for in_progress_status in in_progress_labels: + api_url += "&status=" + in_progress_status + if show_resolved: + for resolved_status in resolved_labels: + api_url += "&status=" + resolved_status + api_url += "&api_key=" + api_key + var header_data : Array = __create_header_data() + var error : int = http_client.request(HTTPClient.METHOD_GET, api_url, header_data) + assert(error == Error.OK) + while http_client.get_status() == HTTPClient.STATUS_REQUESTING: + http_client.poll() + assert(http_client.get_status() == HTTPClient.STATUS_BODY or http_client.get_status() == HTTPClient.STATUS_CONNECTED) + var response_string : String = __get_http_client_chunk_response(http_client) + + var response_data := JSON.parse_string(response_string) + if __validate_server_response(response_data) != Error.OK: + return + + var bug_array : Array = [] + var label_dict : Dictionary = { + "show_unresolved": show_unresolved, + "show_in_progress": show_in_progress, + "show_resolved": show_resolved, + + "unresolved_labels": unresolved_labels, + "in_progress_labels": in_progress_labels, + "resolved_labels": resolved_labels, + + "map_name_field": map_name_field, + "marker_location_field": marker_location_field, + } + for bug_in:Dictionary in response_data["bugs"]: + var bug : BugbotBugData = __create_bug_data_from_server_response(bug_in, map_name, label_dict, true) + if bug: + bug_array.append(bug) + + callback.call_deferred(bug_array) + __bugbot_server_thread.call_deferred("wait_to_finish") + +func __prepare_form_thread(callback:Callable) -> void: + var http_client : HTTPClient = HTTPClient.new() + if __connect_to_server(http_client, ProjectSettings.get_setting("bugbot/reporting/bugzilla/server", DEFAULT_SERVER)) != HTTPClient.STATUS_CONNECTED: + printerr("Could not connect to server.") + return + + var header_data : Array = __create_header_data() + var product_name : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/product_name", DEFAULT_PRODUCT_NAME) + var api_key : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/API_key", DEFAULT_API_KEY) + + var api_url : String = "product/%s?" % [product_name] + api_url += "api_key=" + api_key + var error : int = http_client.request(HTTPClient.METHOD_GET, __build_url_string(api_url), header_data) + assert(error == Error.OK) + while http_client.get_status() == HTTPClient.STATUS_REQUESTING: + http_client.poll() + assert(http_client.get_status() == HTTPClient.STATUS_BODY or http_client.get_status() == HTTPClient.STATUS_CONNECTED) + var product_response_string : String = __get_http_client_chunk_response(http_client) + var product_response : Dictionary = JSON.parse_string(product_response_string) + if __validate_server_response(product_response) != Error.OK: + return + + api_url = "field/bug?" + api_url += "api_key=" + api_key + error = http_client.request(HTTPClient.METHOD_GET, __build_url_string(api_url), header_data) + assert(error == Error.OK) + while http_client.get_status() == HTTPClient.STATUS_REQUESTING: + http_client.poll() + assert(http_client.get_status() == HTTPClient.STATUS_BODY or http_client.get_status() == HTTPClient.STATUS_CONNECTED) + var fields_response_string : String = __get_http_client_chunk_response(http_client) + var fields_response : Dictionary = JSON.parse_string(fields_response_string) + if __validate_server_response(fields_response) != Error.OK: + return + + var tag_lists : Array + tag_lists.resize(BugbotTagArray.MAX) + tag_lists[BugbotTagArray.VERSION] = [] + tag_lists[BugbotTagArray.HARDWARE] = [] + tag_lists[BugbotTagArray.OS] = [] + tag_lists[BugbotTagArray.COMPONENT] = [] + tag_lists[BugbotTagArray.SEVERITY] = [] + + var product_info : Dictionary = product_response["products"][0] + if product_info["name"] != product_name: + printerr("Incorrect product found.") + __bugbot_server_thread.call_deferred("wait_to_finish") + return + + for component:Dictionary in product_info["components"]: + tag_lists[BugbotTagArray.COMPONENT].append({ "name": component["name"], "id": component["sort_key"] }) + for version:Dictionary in product_info["versions"]: + tag_lists[BugbotTagArray.VERSION].append({ "name": version["name"], "id": version["sort_key"] }) + + for field:Dictionary in fields_response["fields"]: + if field.has("values"): + var field_name : String = field["name"] + var field_values : Array = field["values"] + if field_name == "bug_severity": + for severity:Dictionary in field_values: + tag_lists[BugbotTagArray.SEVERITY].append({ "name": severity["name"], "id": severity["sort_key"] }) + elif field_name == "rep_platform": + for platform:Dictionary in field_values: + tag_lists[BugbotTagArray.HARDWARE].append({ "name": platform["name"], "id": platform["sort_key"] }) + elif field_name == "op_sys": + for os:Dictionary in field_values: + tag_lists[BugbotTagArray.OS].append({ "name": os["name"], "id": os["sort_key"] }) + + callback.call_deferred(tag_lists) + __bugbot_server_thread.call_deferred("wait_to_finish") + +func __send_form_data_thread(data:Dictionary, map_name:String, bug_position:Vector3, bug_normal:Vector3, callback:Callable) -> void: + var http_client : HTTPClient = HTTPClient.new() + if __connect_to_server(http_client, ProjectSettings.get_setting("bugbot/reporting/bugzilla/server", DEFAULT_SERVER)) != HTTPClient.STATUS_CONNECTED: + printerr("Could not connect to server.") + return + + var product_name : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/product_name", DEFAULT_PRODUCT_NAME) + var api_key : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/API_key", DEFAULT_API_KEY) + var default_post_status : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/default_status", DEFAULT_POST_STATUS) + var map_name_field : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/map_name_field", DEFAULT_MAP_NAME_FIELD) + var marker_location_field : String = ProjectSettings.get_setting("bugbot/reporting/bugzilla/marker_location_field", DEFAULT_MARKER_LOCATION_FIELD) + var marker_location : String = "%.4f,%.4f,%.4f:%.4f,%.4f,%.4f" % [bug_position.x, bug_position.y, bug_position.z, bug_normal.x, bug_normal.y, bug_normal.z] + + var api_url : String = "bug?" + api_url += "api_key=" + api_key + + var post_data : Dictionary = { + "product": product_name, + map_name_field: map_name, + marker_location_field: marker_location, + "summary": data["title"], + "description": data["body"], + "status": default_post_status, + } + if not data["labels"]["version"].is_empty(): post_data["version"] = data["labels"]["version"]["name"] + if not data["labels"]["hardware"].is_empty(): post_data["platform"] = data["labels"]["hardware"]["name"] + if not data["labels"]["os"].is_empty(): post_data["op_sys"] = data["labels"]["os"]["name"] + if not data["labels"]["component"].is_empty(): post_data["component"] = data["labels"]["component"]["name"] + if not data["labels"]["severity"].is_empty(): post_data["severity"] = data["labels"]["severity"]["name"] + + var post_data_string : String = JSON.stringify(post_data) + + var header_data : Array = __create_header_data(post_data_string.length()) + var error : int = http_client.request(HTTPClient.METHOD_POST, __build_url_string(api_url), header_data, post_data_string) + assert(error == Error.OK) + while http_client.get_status() == HTTPClient.STATUS_REQUESTING: + http_client.poll() + assert(http_client.get_status() == HTTPClient.STATUS_BODY or http_client.get_status() == HTTPClient.STATUS_CONNECTED) + var post_response_string : String = __get_http_client_chunk_response(http_client) + var post_response : Dictionary = JSON.parse_string(post_response_string) + if __validate_server_response(post_response) != Error.OK: + return + + post_data["id"] = post_response["id"] + post_data["is_open"] = true + + var label_dict : Dictionary = { + "show_unresolved": ProjectSettings.get_setting("bugbot/markers/unresolved/show_unresolved_bugs", DEFAULT_SHOW_UNRESOLVED_BUGS), + "show_in_progress": ProjectSettings.get_setting("bugbot/markers/in_progress/show_in_progress_bugs", DEFAULT_SHOW_IN_PROGRESS_BUGS), + "show_resolved": ProjectSettings.get_setting("bugbot/markers/resolved/show_resolved_bugs", DEFAULT_SHOW_RESOLVED_BUGS), + + "unresolved_labels": ProjectSettings.get_setting("bugbot/reporting/bugzilla/status_labels/unresolved_statuses", DEFAULT_UNRESOLVED_STATUSES), + "in_progress_labels": ProjectSettings.get_setting("bugbot/reporting/bugzilla/status_labels/in_progress_statuses", DEFAULT_IN_PROGRESS_STATUSES), + "resolved_labels": ProjectSettings.get_setting("bugbot/reporting/bugzilla/status_labels/resolved_statuses", DEFAULT_RESOLVED_STATUSES), + + "map_name_field": map_name_field, + "marker_location_field": marker_location_field, + } + var bug_data : BugbotBugData = __create_bug_data_from_server_response(post_data, map_name, label_dict, false) + + callback.call_deferred(bug_data) + __bugbot_server_thread.call_deferred("wait_to_finish") + func _current_server_api() -> String: return "Bugzilla" + +func __build_url_string(_api_suffix:String) -> String: + return "/" + \ + ProjectSettings.get_setting("bugbot/reporting/bugzilla/REST_URI", DEFAULT_REST_URI) + \ + "/" + \ + _api_suffix + +func __create_header_data(content_length:int = -1) -> Array: + var header : Array = [ + "User-Agent: Pirulo/1.0 (Godot)", + "Accept: application/json", + ] + header.append("Authorization: token " + ProjectSettings.get_setting("bugbot/reporting/gitea/API_key", DEFAULT_API_KEY)) + if content_length >= 0: + header.append("Content-Type: application/json") + header.append("Content-Length: " + String.num_uint64(content_length)) + return header + +func __validate_server_response(_response:Variant) -> int: + # If the response has a message field, make the assumption that this is + # because the response was an error code. + if (_response.has("error") and _response["error"] == true) or \ + _response.has("message"): + var error_data : BugbotErrorData = BugbotErrorData.new() + error_data.message = _response["message"] + printerr(error_data.message) + __bugbot_server_thread.call_deferred("wait_to_finish") + return Error.FAILED + return Error.OK + +func __create_bug_data_from_server_response(bug_in:Dictionary, map_name:String, label_dict:Dictionary, validate_labels:bool) -> BugbotBugData: + var bug : BugbotBugData = BugbotBugData.new() + + # Check if the map name is valid for the scene we're in. + bug.map_name = bug_in[label_dict["map_name_field"]] + if bug.map_name != map_name: + return null + + var marker_location : String = bug_in[label_dict["marker_location_field"]] + if not marker_location.is_empty(): + var marker_location_parts : PackedStringArray = marker_location.split(":") + if marker_location_parts.size() == 2: + var marker_position : PackedStringArray = marker_location_parts[0].split(",") + var marker_normal : PackedStringArray = marker_location_parts[1].split(",") + if marker_position.size() == 3: + bug.marker_position = Vector3(float(marker_position[0]), float(marker_position[1]), float(marker_position[2])) + if marker_normal.size() == 3: + bug.marker_normal = Vector3(float(marker_normal[0]), float(marker_normal[1]), float(marker_normal[2])) + + var resolved_tag_found : bool = false + var in_progress_tag_found : bool = false + var unresolved_tag_found : bool = false + + # Find which resolution statuses apply to this bug. + bug.is_open = bug_in["is_open"] + bug.status = bug_in["status"] + if not bug.is_open: + resolved_tag_found = true + else: + for label:String in label_dict["resolved_labels"] as Array: + if bug.status == label: resolved_tag_found = true + for label:String in label_dict["in_progress_labels"] as Array: + if bug.status == label: in_progress_tag_found = true + for label:String in label_dict["unresolved_labels"] as Array: + if bug.status == label: unresolved_tag_found = true + + # Figure out which resolution tag to prioritise, and whether we should show the marker. + var show_marker : bool + if resolved_tag_found: + bug.resolution = BugbotServerAPI.RESOLVED_TAG + show_marker = label_dict["show_resolved"] as bool + elif in_progress_tag_found: + bug.resolution = BugbotServerAPI.IN_PROGRESS_TAG + show_marker = label_dict["show_in_progress"] as bool + elif unresolved_tag_found or (label_dict["unresolved_labels"] as Array).is_empty(): + bug.resolution = BugbotServerAPI.UNRESOLVED_TAG + show_marker = label_dict["show_unresolved"] as bool + if validate_labels and not show_marker: + return null + + bug.id = bug_in["id"] + bug.title = bug_in["summary"] + bug.component = bug_in["component"] + bug.platform = bug_in["platform"] + bug.operating_system = bug_in["op_sys"] + bug.severity = bug_in["severity"] + + return bug diff --git a/bugbot.gd b/bugbot.gd index de2cbc6..f333d7d 100644 --- a/bugbot.gd +++ b/bugbot.gd @@ -43,11 +43,16 @@ func __initialise_project_settings() -> void: __add_project_setting("bugbot/reporting/bug_report_widget_depth", TYPE_INT, 10000) #region Bugzilla - __add_project_setting("bugbot/reporting/bugzilla/server", TYPE_STRING, "https://bugzilla.example.com") - __add_project_setting("bugbot/reporting/bugzilla/REST_URI", TYPE_STRING, "rest/") - __add_project_setting("bugbot/reporting/bugzilla/product_name", TYPE_STRING, "") - __add_project_setting("bugbot/reporting/bugzilla/API_key", TYPE_STRING, "") - __add_project_setting("bugbot/reporting/bugzilla/default_status", TYPE_STRING, "UNCONFIRMED") + __add_project_setting("bugbot/reporting/bugzilla/server", TYPE_STRING, BugbotServerBugzillaAPI.DEFAULT_SERVER) + __add_project_setting("bugbot/reporting/bugzilla/REST_URI", TYPE_STRING, BugbotServerBugzillaAPI.DEFAULT_REST_URI) + __add_project_setting("bugbot/reporting/bugzilla/product_name", TYPE_STRING, BugbotServerBugzillaAPI.DEFAULT_PRODUCT_NAME) + __add_project_setting("bugbot/reporting/bugzilla/API_key", TYPE_STRING, BugbotServerBugzillaAPI.DEFAULT_API_KEY) + __add_project_setting("bugbot/reporting/bugzilla/default_status", TYPE_STRING, BugbotServerBugzillaAPI.DEFAULT_POST_STATUS) + __add_project_setting("bugbot/reporting/bugzilla/map_name_field", TYPE_STRING, BugbotServerBugzillaAPI.DEFAULT_MAP_NAME_FIELD) + __add_project_setting("bugbot/reporting/bugzilla/marker_location_field", TYPE_STRING, BugbotServerBugzillaAPI.DEFAULT_MARKER_LOCATION_FIELD) + __add_project_setting("bugbot/reporting/bugzilla/status_labels/unresolved_statuses", TYPE_ARRAY, BugbotServerBugzillaAPI.DEFAULT_UNRESOLVED_STATUSES, PROPERTY_HINT_ARRAY_TYPE, &"21/:") + __add_project_setting("bugbot/reporting/bugzilla/status_labels/in_progress_statuses", TYPE_ARRAY, BugbotServerBugzillaAPI.DEFAULT_IN_PROGRESS_STATUSES, PROPERTY_HINT_ARRAY_TYPE, &"21/:") + __add_project_setting("bugbot/reporting/bugzilla/status_labels/resolved_statuses", TYPE_ARRAY, BugbotServerBugzillaAPI.DEFAULT_RESOLVED_STATUSES, PROPERTY_HINT_ARRAY_TYPE, &"21/:") #endregion #region Gitea __add_project_setting("bugbot/reporting/gitea/server", TYPE_STRING, BugbotServerGiteaAPI.DEFAULT_SERVER)