diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm index 3137088d1c90..29895088200c 100644 --- a/code/__DEFINES/admin.dm +++ b/code/__DEFINES/admin.dm @@ -48,6 +48,7 @@ GLOBAL_LIST_INIT(note_categories, list("Admin", "Merit", "Whitelist")) #define OBSERVER_JMP(observer, atom) atom ? "(JMP)" : "" #define ARES_MARK(user) "(MARK)" #define ARES_REPLY(user, ref) "(RPLY)" +#define ADMIN_VIEW_BUG_REPORT(datum) "VIEW REPORT" /atom/proc/Admin_Coordinates_Readable(area_name, admin_jump_ref) var/turf/T = get_turf(src) diff --git a/code/_globalvars/global_lists.dm b/code/_globalvars/global_lists.dm index 2bd24af5a0ff..b2c56cf1ffa5 100644 --- a/code/_globalvars/global_lists.dm +++ b/code/_globalvars/global_lists.dm @@ -9,6 +9,9 @@ GLOBAL_LIST_EMPTY(CLFFaxes) GLOBAL_LIST_EMPTY(GeneralFaxes) //Inter-machine faxes GLOBAL_LIST_EMPTY(fax_contents) //List of fax contents to maintain it even if source paper is deleted +// for all of our various bugs and runtimes +GLOBAL_LIST_EMPTY(bug_reports) + //datum containing a reference to the flattend map png url, the actual png is stored in the user's cache. GLOBAL_LIST_EMPTY(uscm_flat_tacmap_data) GLOBAL_LIST_EMPTY(xeno_flat_tacmap_data) diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 1cf93e998a4e..627859369231 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -664,3 +664,11 @@ This maintains a list of ip addresses that are able to bypass topic filtering. /datum/config_entry/string/client_error_message default = "Your version of BYOND is too old, may have issues, and is blocked from accessing this server." + +// GitHub API, used for anonymous bug report handling. +/datum/config_entry/string/github_app_api + protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN + +/datum/config_entry/string/repo_name + +/datum/config_entry/string/org diff --git a/code/datums/bug_report.dm b/code/datums/bug_report.dm new file mode 100644 index 000000000000..1812f6f61916 --- /dev/null +++ b/code/datums/bug_report.dm @@ -0,0 +1,189 @@ +// Datum for handling bug reports +#define STATUS_SUCCESS 201 + +/datum/tgui_bug_report_form + /// contains all the body text for the bug report. + var/list/bug_report_data = null + + /// client of user who created the initial report, immutable, set on init. + var/client/initial_user = null + + /// client of the admin who is accessing the report, we don't want multiple admins unknowingly making changes at the same time. + var/client/admin_user = null + + /// value to determine if the bug report is submitted and awaiting admin approval, used for state purposes in tgui. + var/awaiting_admin_approval = FALSE + + // for garbage collection purposes. + var/selected_confirm = FALSE + +/datum/tgui_bug_report_form/New(mob/user) + initial_user = user.client + +/datum/tgui_bug_report_form/proc/external_link_prompt(client/user) + tgui_alert(user, "Unable to create a bug report at this time, please create the issue directly through our GitHub repository instead") + var/url = CONFIG_GET(string/githuburl) + if(!url) + to_chat(user, SPAN_WARNING("The configuration is not properly set, unable to open external link")) + return + + if(tgui_alert(user, "This will open the GitHub in your browser. Are you sure?", "Confirm", list("Yes", "No")) == "Yes") + user << link(url) + +/datum/tgui_bug_report_form/ui_state() + return GLOB.always_state + +/datum/tgui_bug_report_form/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "BugReportForm") + ui.open() + +/datum/tgui_bug_report_form/ui_close(mob/user) + . = ..() + if(!admin_user && user.client == initial_user && !selected_confirm) // user closes the ui without selecting confirm or approve. + qdel(src) + return + admin_user = null + selected_confirm = FALSE + +/datum/tgui_bug_report_form/Destroy() + GLOB.bug_reports -= src + return ..() + +/datum/tgui_bug_report_form/proc/sanitize_payload(list/params) + for(var/param in params) + params[param] = sanitize(params[param], list("\t"=" ","�"=" ")) + + return params + +// whether or not an admin can access the record at a given time. +/datum/tgui_bug_report_form/proc/assign_admin(mob/user) + if(!initial_user) + to_chat(user, SPAN_WARNING("Unable to identify the author of the bug report.")) + return FALSE + if(admin_user) + if(user.client == admin_user) + to_chat(user, SPAN_WARNING("This bug report review is already opened and accessed by you.")) + else + to_chat(user, SPAN_WARNING("Another administrator is currently accessing this report, please wait for them to finish before making any changes.")) + return FALSE + if(!CLIENT_IS_STAFF(user.client)) + message_admins("[user.ckey] has attempted to review [initial_user.ckey]'s bug report titled [bug_report_data["title"]] without proper authorization at [time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")].") + return FALSE + + admin_user = user.client + return TRUE + +// returns the body payload +/datum/tgui_bug_report_form/proc/create_form() + var/datum/getrev/revdata = GLOB.revdata + var/test_merges + if(length(revdata.testmerge)) + test_merges = revdata.GetTestMergeInfo(header = FALSE) + + var/desc = {" +### Testmerges +[test_merges ? test_merges : "N/A"] + +### Round ID +[GLOB.round_id ? GLOB.round_id : "N/A"] + +### Description of the bug +[bug_report_data["description"]] + +### What's the difference with what should have happened? +[bug_report_data["expected_behavior"]] + +### How do we reproduce this bug? +[bug_report_data["steps"]] + +## Report details +Author: [initial_user] +Admin: [admin_user] + "} + + return desc + +// the real deal, we are sending the request through the api. +/datum/tgui_bug_report_form/proc/send_request(payload_body, client/user) + // for any future changes see https://docs.github.com/en/rest/issues/issues + var/repo_name = CONFIG_GET(string/repo_name) + var/org = CONFIG_GET(string/org) + var/token = CONFIG_GET(string/github_app_api) + + if(!token || !org || !repo_name) + tgui_alert(user, "The configuration is not set for the external API.", "Issue not reported!") + external_link_prompt(user) + qdel(src) + return + + var/url = "https://api.github.com/repos/[org]/[repo_name]/issues" + var/list/headers = list() + headers["Authorization"] = "Bearer [token]" + headers["Content-Type"] = "text/markdown; charset=utf-8" + headers["Accept"] = "application/vnd.github+json" + + var/datum/http_request/request = new() + var/list/payload = list( + "title" = bug_report_data["title"], + "body" = payload_body, + "labels" = list("Bug") + ) + + request.prepare(RUSTG_HTTP_METHOD_POST, url, json_encode(payload), headers) + request.begin_async() + UNTIL(request.is_complete()) + + var/datum/http_response/response = request.into_response() + if(response.errored || response.status_code != STATUS_SUCCESS) + external_link_prompt(user) + else + message_admins("[user.ckey] has approved a bug report from [initial_user.ckey] titled [bug_report_data["title"]] at [time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")].") + to_chat(initial_user, SPAN_WARNING("An admin has successfully submitted your report and it should now be visible on GitHub. Thanks again!")) + qdel(src)// approved and submitted, we no longer need the datum. + +// proc that creates a ticket for an admin to approve or deny a bug report request +/datum/tgui_bug_report_form/proc/bug_report_request() + to_chat(initial_user, SPAN_WARNING("Your bug report has been submitted, thank you!")) + GLOB.bug_reports += src + + var/general_message = "[initial_user.ckey] has created a bug report, you may find this report directly in the ticket panel. Feel free modify the issue to your liking before submitting it to GitHub." + GLOB.admin_help_ui_handler.perform_adminhelp(initial_user, general_message, urgent = FALSE) + + var/href_message = ADMIN_VIEW_BUG_REPORT(src) + initial_user.current_ticket.AddInteraction(href_message) + +/datum/tgui_bug_report_form/ui_act(action, list/params, datum/tgui/ui) + . = ..() + if (.) + return + var/mob/user = ui.user + switch(action) + if("confirm") + bug_report_data = sanitize_payload(params) + selected_confirm = TRUE + // bug report request is now waiting for admin approval + if(!awaiting_admin_approval) + bug_report_request() + awaiting_admin_approval = TRUE + else // otherwise it's been approved + var/payload_body = create_form() + send_request(payload_body, user.client) + if("cancel") + if(awaiting_admin_approval) // admin has chosen to reject the bug report + reject(user.client) + qdel(src) + ui.close() + . = TRUE + +/datum/tgui_bug_report_form/ui_data(mob/user) + . = list() + .["report_details"] = bug_report_data // only filled out once the user as submitted the form + .["awaiting_admin_approval"] = awaiting_admin_approval + +/datum/tgui_bug_report_form/proc/reject(client/user) + message_admins("[user.ckey] has rejected a bug report from [initial_user.ckey] titled [bug_report_data["title"]] at [time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")].") + to_chat(initial_user, SPAN_WARNING("An admin has rejected your bug report, this can happen for several reasons. They will most likely get back to you shortly regarding your issue.")) + +#undef STATUS_SUCCESS diff --git a/code/modules/admin/topic/topic.dm b/code/modules/admin/topic/topic.dm index 000d94e70866..f3af6ed440e9 100644 --- a/code/modules/admin/topic/topic.dm +++ b/code/modules/admin/topic/topic.dm @@ -2248,6 +2248,21 @@ return return remove_tagged_datum(datum_to_remove) + if(href_list["view_bug_report"]) + if(!check_rights(R_ADMIN|R_MOD)) + return + + var/datum/tgui_bug_report_form/bug_report = locate(href_list["view_bug_report"]) + if(!istype(bug_report) || QDELETED(bug_report)) + to_chat(usr, SPAN_WARNING("This bug report is no longer available.")) + return + + if(!bug_report.assign_admin(usr)) + return + + bug_report.tgui_interact(usr) + return + if(href_list["show_tags"]) if(!check_rights(R_ADMIN)) return diff --git a/colonialmarines.dme b/colonialmarines.dme index ce7c050d6dba..e4b01ddc8453 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -332,6 +332,7 @@ #include "code\datums\ASRS.dm" #include "code\datums\beam.dm" #include "code\datums\browser.dm" +#include "code\datums\bug_report.dm" #include "code\datums\callback.dm" #include "code\datums\changelog.dm" #include "code\datums\combat_personalized.dm" diff --git a/config/example/config.txt b/config/example/config.txt index 0aff7ee6def9..d63e6822465c 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -253,3 +253,8 @@ GAMEMODE_DEFAULT Extended CLIENT_ERROR_VERSION 514 #CLIENT_ERROR_BUILD 1589 #CLIENT_ERROR_MESSAGE Your version of BYOND is too old, may have issues, and is blocked from accessing this server. + +## GITHUB API +#GITHUB_APP_API +#REPO_NAME cmss13 +#ORG cmss13-devs \ No newline at end of file diff --git a/interface/interface.dm b/interface/interface.dm index c9112160d94f..5b30eaa53bf7 100644 --- a/interface/interface.dm +++ b/interface/interface.dm @@ -62,14 +62,11 @@ set name = "Submit Bug" set desc = "Submit a bug." set hidden = TRUE - - if(tgui_alert(src, "Please search for the bug first to make sure you aren't posting a duplicate.", "No dupe bugs please", list("OK", "Cancel")) != "OK") - return - - if(tgui_alert(src, "This will open the GitHub in your browser. Are you sure?", "Confirm", list("Yes", "No")) != "Yes") + if(!usr) return + var/datum/tgui_bug_report_form/report = new(usr) - src << link(CONFIG_GET(string/githuburl)) + report.tgui_interact(usr) return /client/verb/set_fps() diff --git a/tgui/packages/tgui/interfaces/BugReportForm.tsx b/tgui/packages/tgui/interfaces/BugReportForm.tsx new file mode 100644 index 000000000000..f5b3b71bea23 --- /dev/null +++ b/tgui/packages/tgui/interfaces/BugReportForm.tsx @@ -0,0 +1,175 @@ +import { BooleanLike } from 'common/react'; +import React, { useState } from 'react'; + +import { useBackend } from '../backend'; +import { Flex, Section } from '../components'; +import { ButtonCheckbox } from '../components/Button'; +import { Window } from '../layouts'; +interface FormTypes { + awaiting_admin_approval: BooleanLike; + report_details: FormDetails; +} + +// all the information necessary to pass into the github api +type FormDetails = { + steps: string; + title: string; + description: string; + expected_behavior: string; +}; + +const InputTitle = (props) => { + return ( +

+ {props.children} + {props.required && {' *'}} +

+ ); +}; + +export const BugReportForm = (props) => { + const { act, data } = useBackend(); + const { awaiting_admin_approval, report_details } = data; + const [checkBox, setCheckbox] = useState(false); + + const [title, setTitle] = useState(report_details?.title || ''); + const [steps, setSteps] = useState(report_details?.steps || ''); + const [description, setDescription] = useState( + report_details?.description || '', + ); + const [expected_behavior, setExpectedBehavior] = useState( + report_details?.expected_behavior || '', + ); + + const submit = () => { + if (!title || !description || !expected_behavior || !steps || !checkBox) { + alert('Please fill out all required fields!'); + return; + } + const updatedReportDetails = { + title, + steps, + description, + expected_behavior, + }; + act('confirm', updatedReportDetails); + }; + + return ( + + +
+ + + + GitHub Repository + + + +

+ { + 'TIP: please be as descriptive as possible, it really does help tremendously' + } +

+
+ + {'Title'} + setTitle(e.target.value)} + /> + + + {'Description'} + {'Give a description of the bug'} +