class_name BugbotServerJiraAPI extends "res://addons/Bugbot/Scripts/server_api.gd" ##region Consts const CHILD_OPTION_SEPARATOR : StringName = &" - " const DEFAULT_SERVER : StringName = &"https://jira.example.com" const DEFAULT_REST_URI : StringName = &"rest/" const DEFAULT_PROJECT_NAME : StringName = &"ProjectName" const DEFAULT_EMAIL : StringName = &"jira@example.com" const DEFAULT_API_KEY : StringName = &"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234=12345678" const DEFAULT_BUG_ISSUE_TYPE : StringName = &"Bug" const DEFAULT_MAP_NAME_FIELD : StringName = &"Map Name" const DEFAULT_MARKER_LOCATION_FIELD : StringName = &"Marker Location" const DEFAULT_DEPARTMENT_FIELD : StringName = &"" const DEFAULT_SEVERITY_FIELD : StringName = &"" const DEFAULT_PLATFORM_FIELD : StringName = &"" const DEFAULT_OS_FIELD : StringName = &"" const DEFAULT_VERSION_FIELD : StringName = &"" const DEFAULT_UNRESOLVED_STATUSES : Array = [&"To Do"] const DEFAULT_IN_PROGRESS_STATUSES : Array = [&"In Progress"] const DEFAULT_RESOLVED_STATUSES : Array = [&"Done"] #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/jira/server", DEFAULT_SERVER)) != HTTPClient.STATUS_CONNECTED: printerr("Could not connect to server.") return var header_data : Array = __create_header_data() var server_data : BugbotJiraServerData = __get_server_data(http_client, header_data) if not server_data: __bugbot_server_thread.call_deferred("wait_to_finish") return var map_name_field : String = ProjectSettings.get_setting("bugbot/reporting/jira/map_name_field", DEFAULT_MAP_NAME_FIELD) var marker_location_field : String = ProjectSettings.get_setting("bugbot/reporting/jira/marker_location_field", DEFAULT_MARKER_LOCATION_FIELD) var bug_issue_type : String = ProjectSettings.get_setting("bugbot/reporting/jira/bug_issue_type", DEFAULT_BUG_ISSUE_TYPE) var show_unresolved : bool = ProjectSettings.get_setting("bugbot/markers/unresolved/show_unresolved_bugs", BugbotServerAPI.DEFAULT_SHOW_UNRESOLVED_BUGS) var show_in_progress : bool = ProjectSettings.get_setting("bugbot/markers/in_progress/show_in_progress_bugs", BugbotServerAPI.DEFAULT_SHOW_IN_PROGRESS_BUGS) var show_resolved : bool = ProjectSettings.get_setting("bugbot/markers/resolved/show_resolved_bugs", BugbotServerAPI.DEFAULT_SHOW_RESOLVED_BUGS) var unresolved_labels : Array = ProjectSettings.get_setting("bugbot/reporting/jira/status_labels/unresolved_statuses", DEFAULT_UNRESOLVED_STATUSES) var in_progress_labels : Array = ProjectSettings.get_setting("bugbot/reporting/jira/status_labels/in_progress_statuses", DEFAULT_IN_PROGRESS_STATUSES) var resolved_labels : Array = ProjectSettings.get_setting("bugbot/reporting/jira/status_labels/resolved_statuses", DEFAULT_RESOLVED_STATUSES) var jql_label_query_array : PackedStringArray = [] if show_unresolved: for label:String in unresolved_labels: jql_label_query_array.append("status=\"%s\"" % [label]) if show_in_progress: for label:String in in_progress_labels: jql_label_query_array.append("status=\"%s\"" % [label]) if show_resolved: for label:String in resolved_labels: jql_label_query_array.append("status=\"%s\"" % [label]) var jql_label_query : String = " OR ".join(jql_label_query_array) var url_string : String = "api/2/search" var post_data : Dictionary = { "jql": "project=\"%s\" AND type=%s AND (%s) AND \"%s\"~\"%s\" AND \"%s\" is not EMPTY" % [server_data.project_name, bug_issue_type, jql_label_query, map_name_field, map_name, marker_location_field], "fields": ["summary", "description", "issuetype", "status"] } if not server_data.map_name_field_key.is_empty(): post_data["fields"].append(server_data.map_name_field_key) if not server_data.marker_location_field_key.is_empty(): post_data["fields"].append(server_data.marker_location_field_key) if not server_data.severity_field_key.is_empty(): post_data["fields"].append(server_data.severity_field_key) if not server_data.department_field_key.is_empty(): post_data["fields"].append(server_data.department_field_key) if not server_data.platform_field_key.is_empty(): post_data["fields"].append(server_data.platform_field_key) if not server_data.os_field_key.is_empty(): post_data["fields"].append(server_data.os_field_key) if not server_data.version_field_key.is_empty(): post_data["fields"].append(server_data.version_field_key) var post_data_string : String = JSON.stringify(post_data) header_data = __create_header_data(post_data_string.length()) var error : int = http_client.request(HTTPClient.METHOD_POST, __build_url_string(url_string), 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 issue_response_string : String = __get_http_client_chunk_response(http_client) var issue_response : Dictionary = JSON.parse_string(issue_response_string) if __validate_server_response(issue_response) != Error.OK: return var bug_array : Array if issue_response: var issues_array : Array = issue_response["issues"] for issue:Dictionary in issues_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, } var bug : BugbotBugData = __create_bug_data_from_server_response(issue, map_name, label_dict, server_data, 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/jira/server", DEFAULT_SERVER)) != HTTPClient.STATUS_CONNECTED: printerr("Could not connect to server.") return var header_data : Array = __create_header_data() var server_data : BugbotJiraServerData = __get_server_data(http_client, header_data) if not server_data: __bugbot_server_thread.call_deferred("wait_to_finish") 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 severity_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/status_labels/severity_field", DEFAULT_SEVERITY_FIELD) var department_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/department_field", DEFAULT_DEPARTMENT_FIELD) var platform_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/platform_field", DEFAULT_PLATFORM_FIELD) var os_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/os_field", DEFAULT_OS_FIELD) var version_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/version_field", DEFAULT_VERSION_FIELD) for field:Dictionary in server_data.fields_response: var field_name : String = (field["name"] as String) if field_name == severity_field_name: for value:Dictionary in field["allowedValues"]: tag_lists[BugbotTagArray.SEVERITY].append({ "name": value["value"], "id": int(value["id"]) }) continue if field_name == department_field_name: for value:Dictionary in field["allowedValues"]: tag_lists[BugbotTagArray.COMPONENT].append({ "name": value["value"], "id": int(value["id"]) }) continue if field_name == platform_field_name: for value:Dictionary in field["allowedValues"]: if value.has("children"): for child:Dictionary in value["children"]: tag_lists[BugbotTagArray.HARDWARE].append({ "name": value["value"] + CHILD_OPTION_SEPARATOR + child["value"], "id": int(value["id"]), "child_name": child["value"], "child_id": int(child["id"]) }) else: tag_lists[BugbotTagArray.HARDWARE].append({ "name": value["value"], "id": int(value["id"]) }) continue if field_name == os_field_name: for value:Dictionary in field["allowedValues"]: tag_lists[BugbotTagArray.OS].append({ "name": value["value"], "id": int(value["id"]) }) continue if field_name == version_field_name: for value:Dictionary in field["allowedValues"]: tag_lists[BugbotTagArray.VERSION].append({ "name": value["name"], "id": int(value["id"]) }) continue 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/jira/server", DEFAULT_SERVER)) != HTTPClient.STATUS_CONNECTED: printerr("Could not connect to server.") return var header_data : Array = __create_header_data() var server_data : BugbotJiraServerData = __get_server_data(http_client, header_data) if not server_data: __bugbot_server_thread.call_deferred("wait_to_finish") return var bug_issue_type : String = ProjectSettings.get_setting("bugbot/reporting/jira/bug_issue_type", DEFAULT_BUG_ISSUE_TYPE) 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 post_data : Dictionary = { "fields": { "project": { "key" : server_data.project_key }, "summary": data["title"], "description": data["body"], "issuetype": { "name": bug_issue_type }, server_data.map_name_field_key: map_name, server_data.marker_location_field_key: marker_location, } } if data["labels"].has("severity"): post_data["fields"][server_data.severity_field_key] = { "value": data["labels"]["severity"]["name"] } if data["labels"].has("version"): post_data["fields"][server_data.version_field_key] = { "value": data["labels"]["version"]["name"] } if data["labels"].has("os"): post_data["fields"][server_data.os_field_key] = { "value": data["labels"]["os"]["name"] } if data["labels"].has("component"): post_data["fields"][server_data.department_field_key] = { "value": data["labels"]["component"]["name"] } if data["labels"].has("hardware"): post_data["fields"][server_data.platform_field_key] = { "value": data["labels"]["hardware"]["name"] } if data["labels"]["hardware"].has("child_name"): post_data["fields"][server_data.platform_field_key]["value"] = post_data["fields"][server_data.platform_field_key]["value"].split(CHILD_OPTION_SEPARATOR)[0] post_data["fields"][server_data.platform_field_key]["child"] = { "value": data["labels"]["hardware"]["child_name"] } var post_data_string : String = JSON.stringify(post_data) header_data = __create_header_data(post_data_string.length()) var error : int = http_client.request(HTTPClient.METHOD_POST, __build_url_string("api/2/issue"), 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_data : Variant = JSON.parse_string(__get_http_client_chunk_response(http_client)) if __validate_server_response(post_response_data) != Error.OK: return var bug_data : BugbotBugData = BugbotBugData.new() bug_data.id = int(post_response_data["id"]) bug_data.title = post_data["fields"]["summary"] bug_data.body = post_data["fields"]["description"] bug_data.map_name = map_name bug_data.marker_position = bug_position bug_data.marker_normal = bug_normal if post_data["fields"].has(server_data.platform_field_key): bug_data.platform = post_data["fields"][server_data.platform_field_key]["value"] if post_data["fields"].has(server_data.severity_field_key): bug_data.platform = post_data["fields"][server_data.severity_field_key]["value"] bug_data.resolution = BugbotServerAPI.UNRESOLVED_TAG callback.call_deferred(bug_data) __bugbot_server_thread.call_deferred("wait_to_finish") func _current_server_api() -> String: return "Jira" func _get_project_name() -> String: return ProjectSettings.get_setting("bugbot/reporting/jira/project_name", DEFAULT_PROJECT_NAME) func _get_bug_url(bug_data:BugbotBugData) -> String: var jira_server : String = ProjectSettings.get_setting("bugbot/reporting/jira/server", DEFAULT_SERVER) return "%s/browse/%s" % [jira_server, bug_data.key] func __build_url_string(_api_suffix:String) -> String: return "/" + \ ProjectSettings.get_setting("bugbot/reporting/jira/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: Basic " + __generate_auth_string()) if content_length >= 0: header.append("Content-Type: application/json") header.append("Content-Length: " + String.num_uint64(content_length)) return header func __generate_auth_string() -> String: var email : String = ProjectSettings.get_setting("bugbot/reporting/jira/email", DEFAULT_EMAIL) var api_key : String = ProjectSettings.get_setting("bugbot/reporting/jira/API_key", DEFAULT_API_KEY) return Marshalls.utf8_to_base64(email.to_lower() + ":" + api_key) func __validate_server_response(_response:Variant) -> int: # If the response has an errorMessages field, make the assumption that this # is because the response was an error code. if _response.has("errors"): if _response["errors"] is Dictionary: for m:String in (_response["errors"] as Dictionary).values(): printerr(m) return Error.FAILED for message:Variant in _response["errors"]: var error_data : BugbotErrorData = BugbotErrorData.new() error_data.code = 1 error_data.message = message printerr(error_data.message) __bugbot_server_thread.call_deferred("wait_to_finish") return Error.FAILED if _response.has("errorMessages"): for message:String in _response["errorMessages"]: var error_data : BugbotErrorData = BugbotErrorData.new() error_data.code = 1 error_data.message = 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, server_data:BugbotJiraServerData, validate_labels:bool) -> BugbotBugData: var bug : BugbotBugData = BugbotBugData.new() var fields : Dictionary = bug_in["fields"] var status : Dictionary = fields["status"] var status_name : String = status["name"] if not fields.has(server_data.map_name_field_key) or fields[server_data.map_name_field_key].is_empty() or \ not fields.has(server_data.marker_location_field_key) or fields[server_data.marker_location_field_key].is_empty(): return null if not fields.has(server_data.map_name_field_key) or fields[server_data.map_name_field_key] != map_name: return null bug.map_name = fields[server_data.map_name_field_key] var position_and_normal : PackedStringArray = (fields[server_data.marker_location_field_key] as String).split(":") if position_and_normal.size() != 2: printerr("Invalid data found for ", server_data.marker_location_field_key) return null var position : PackedStringArray = position_and_normal[0].split(",") var normal : PackedStringArray = position_and_normal[1].split(",") if position.size() < 3 or normal.size() < 3: printerr("Invalid data found for ", server_data.marker_location_field_key) return null bug.marker_position = Vector3(float(position[0]), float(position[1]), float(position[2])) bug.marker_normal = Vector3(float(normal[0]), float(normal[1]), float(normal[2])) # Find which statuses apply to this bug. var resolved_tag_found : bool = false var in_progress_tag_found : bool = false var unresolved_tag_found : bool = false if (label_dict["resolved_labels"] as Array).has(status_name): resolved_tag_found = true if (label_dict["in_progress_labels"] as Array).has(status_name): in_progress_tag_found = true if (label_dict["unresolved_labels"] as Array).has(status_name): 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 bug.is_open = false show_marker = label_dict["show_resolved"] as bool elif in_progress_tag_found: bug.resolution = BugbotServerAPI.IN_PROGRESS_TAG bug.is_open = true 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 bug.is_open = true show_marker = label_dict["show_unresolved"] as bool if validate_labels and not show_marker: return null bug.id = bug_in["id"] bug.key = bug_in["key"] bug.title = fields["summary"] if fields["description"]: bug.body = fields["description"] bug.component = &"Unused" bug.platform = &"Unused" bug.operating_system = &"Unused" bug.severity = fields[server_data.severity_field_key]["value"] bug.status = status_name return bug func __get_server_data(http_client:HTTPClient, header_data:Array) -> BugbotJiraServerData: var server_data : BugbotJiraServerData = BugbotJiraServerData.new() server_data.timestamp = Time.get_ticks_msec() var api_url : String = __build_url_string("api/2/project") 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 project_response_string : String = __get_http_client_chunk_response(http_client) var project_response : Array = JSON.parse_string(project_response_string) if __validate_server_response(project_response) != Error.OK: return server_data.project_name = _get_project_name() for project:Dictionary in project_response: if project["name"] == server_data.project_name: server_data.project_id = project["id"] server_data.project_key = project["key"] server_data.project_avatar_uri = project["avatarUrls"]["48x48"] break if server_data.project_id < 0: printerr(server_data.project_name + " could not not found.") return null # Send a query to retrieve issue types api_url = "api/2/issue/createmeta/%d/issuetypes" % [server_data.project_id] 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 issue_response_string : String = __get_http_client_chunk_response(http_client) var issue_response : Dictionary = JSON.parse_string(issue_response_string) if __validate_server_response(issue_response) != Error.OK: return var bug_issue_type : String = ProjectSettings.get_setting("bugbot/reporting/jira/bug_issue_type", DEFAULT_BUG_ISSUE_TYPE) var bug_issue_id : int = -1 for issue_type:Dictionary in issue_response["issueTypes"]: if issue_type["name"] == bug_issue_type: bug_issue_id = int(issue_type["id"]) break # Send a query to retrieve field options api_url = "api/2/issue/createmeta/%d/issuetypes/%d" % [server_data.project_id, bug_issue_id] 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 map_name_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/map_name_field", DEFAULT_MAP_NAME_FIELD) var marker_location_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/marker_location_field", DEFAULT_MARKER_LOCATION_FIELD) var severity_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/status_labels/severity_field", DEFAULT_SEVERITY_FIELD) var platform_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/platform_field", DEFAULT_PLATFORM_FIELD) var department_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/department_field", DEFAULT_DEPARTMENT_FIELD) var version_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/version_field", DEFAULT_VERSION_FIELD) if fields_response.has("fields"): for field:Dictionary in fields_response["fields"]: var field_name : String = field["name"] if field_name == map_name_field_name: server_data.map_name_field_key = field["key"] continue if field_name == marker_location_field_name: server_data.marker_location_field_key = field["key"] continue if field_name == severity_field_name: server_data.severity_field_key = field["key"] continue if field_name == platform_field_name: server_data.platform_field_key = field["key"] continue if field_name == department_field_name: server_data.department_field_key = field["key"] continue if field_name == version_field_name: server_data.version_field_key = field["key"] continue server_data.fields_response = fields_response["fields"] var map_name_error : bool = false var marker_location_error : bool = false if server_data.map_name_field_key.is_empty(): printerr("Must have a custom field named " + map_name_field_name) map_name_error = true if server_data.marker_location_field_key.is_empty(): printerr("Must have a custom field named " + marker_location_field_name) marker_location_error = true if map_name_error or marker_location_error: return null return server_data class BugbotJiraServerData: var project_name : String var project_id : int = -1 var project_key : String var project_avatar_uri : String var map_name_field_key : String var marker_location_field_key : String var department_field_key : String var severity_field_key : String var platform_field_key : String var os_field_key : String var version_field_key : String var fields_response : Array var timestamp : int