class_name BugbotServerJiraAPI extends "res://addons/Bugbot/Scripts/server_api.gd" ##region Consts 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_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) var project_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/project_name", DEFAULT_PROJECT_NAME) var url_string : String = "api/2/search" var post_data : Dictionary = { "jql": "project=\"%s\"" % [project_name], "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.department_field_key.is_empty(): post_data["fields"].append(server_data.department_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.platform_field_key.is_empty(): post_data["fields"].append(server_data.platform_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 bug_array : Array var issue_response : Dictionary = JSON.parse_string(issue_response_string) if issue_response: var issues_array : Array = issue_response["issues"] for issue:Dictionary in issues_array: var label_dict : Dictionary = { "show_unresolved": ProjectSettings.get_setting("bugbot/markers/unresolved/show_unresolved_bugs", true), "show_in_progress": ProjectSettings.get_setting("bugbot/markers/in_progress/show_in_progress_bugs", true), "show_resolved": ProjectSettings.get_setting("bugbot/markers/resolved/show_resolved_bugs", false), "unresolved_labels": ProjectSettings.get_setting("bugbot/reporting/jira/status_labels/unresolved_statuses", DEFAULT_UNRESOLVED_STATUSES), "in_progress_labels": ProjectSettings.get_setting("bugbot/reporting/jira/status_labels/in_progress_statuses", DEFAULT_IN_PROGRESS_STATUSES), "resolved_labels": ProjectSettings.get_setting("bugbot/reporting/jira/status_labels/resolved_statuses", DEFAULT_RESOLVED_STATUSES), } 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) # Send a query to retrieve issue types var url_string : String = "api/2/issue/createmeta/%d/issuetypes?" % [server_data.project_id] var error : int = http_client.request(HTTPClient.METHOD_GET, __build_url_string(url_string), 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) 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 url_string = "api/2/issue/createmeta/%d/issuetypes/%d" % [server_data.project_id, bug_issue_id] error = http_client.request(HTTPClient.METHOD_GET, __build_url_string(url_string), 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 field_response_string : String = __get_http_client_chunk_response(http_client) var field_response : Dictionary = JSON.parse_string(field_response_string) 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 version_prefix : String = ProjectSettings.get_setting("bugbot/reporting/gitea/status_labels/version_label_prefix", DEFAULT_VERSION_LABEL_PREFIX) var platform_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/platform_field", DEFAULT_PLATFORM_FIELD) var severity_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/severity_field", DEFAULT_SEVERITY_FIELD) for field:Dictionary in field_response["fields"]: if (field["name"] as String) == 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"] as String) == platform_field_name: # for value:Dictionary in field["allowedValues"]: # tag_lists[BugbotTagArray.OS].append(value["value"]) # 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) var project_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/project_name", DEFAULT_PROJECT_NAME) 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"] } 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 __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"): for message:String 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 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 = 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) api_url = __build_url_string("api/2/field") error = 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 fields_response_string : String = __get_http_client_chunk_response(http_client) server_data.project_name = ProjectSettings.get_setting("bugbot/reporting/jira/project_name", DEFAULT_PROJECT_NAME) var project_response : Array = JSON.parse_string(project_response_string) 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 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/optional_fields/severity_field", DEFAULT_SEVERITY_FIELD) var platform_field_name : String = ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/platform_field", DEFAULT_PLATFORM_FIELD) var fields_response : Array = JSON.parse_string(fields_response_string) for field:Dictionary in fields_response: if field.has("scope") and int(field["scope"]["project"]["id"]) != server_data.project_id: continue 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 == ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/department_field", DEFAULT_DEPARTMENT_FIELD): # server_data.department_field_key = field["key"] # continue # if field_name == ProjectSettings.get_setting("bugbot/reporting/jira/optional_fields/version_field", DEFAULT_VERSION_FIELD): # server_data.version_field_key = field["key"] # continue 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 version_field_key : String var timestamp : int