From b9a028679947c4a35b3fbcd1e2035c34167fbff8 Mon Sep 17 00:00:00 2001 From: Vero <73014819+vero5123@users.noreply.github.com> Date: Wed, 19 Jun 2024 06:17:36 -0700 Subject: [PATCH] Adds in-game bug reports without needing a GitHub account (#6392) # About the pull request title # Explain why it's good for the game Essentially, the idea is to allow for the creation of bug reports in-game without needing to own a Github account, this can help to incentivize issue reporting and be beneficial overall. TODO: - [x] Implement the Github API token to allow for anonymous bug reports - [x] Make in-game bug reports admin approval only to prevent abuse and to make sure only quality bug reports are approved. Notable changes from Goonstation's bug report system is that I migrated it to type script and it now requires admin approval. # Changelog Although I made alterations to meet our needs, all credit goes to Pali for their amazing work and to the devs at Goonstation. :cl: add: Adds the ability for users to make bug reports in-game ui: New bug report system ui /:cl: --------- Co-authored-by: DOOM Co-authored-by: harryob <55142896+harryob@users.noreply.github.com> Co-authored-by: Drathek <76988376+Drulikar@users.noreply.github.com> --- code/__DEFINES/admin.dm | 1 + code/__HELPERS/unsorted.dm | 21 +- code/_globalvars/global_lists.dm | 3 + .../configuration/entries/general.dm | 8 + code/datums/bug_report.dm | 202 ++++++++++++++++ code/modules/admin/topic/topic.dm | 15 ++ colonialmarines.dme | 1 + config/example/config.txt | 5 + interface/interface.dm | 9 +- .../tgui/interfaces/BugReportForm.tsx | 217 ++++++++++++++++++ .../tgui/styles/interfaces/BugReportForm.scss | 67 ++++++ tgui/packages/tgui/styles/main.scss | 1 + 12 files changed, 535 insertions(+), 15 deletions(-) create mode 100644 code/datums/bug_report.dm create mode 100644 tgui/packages/tgui/interfaces/BugReportForm.tsx create mode 100644 tgui/packages/tgui/styles/interfaces/BugReportForm.scss 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/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 068d85a71ba5..5b9154eac655 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -1503,15 +1503,18 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) /// Macro for cases where an UNTIL() may go on forever (such as for an http request) #define UNTIL_OR_TIMEOUT(X, __time) \ - do {\ - __time = max(__time, 0);\ - var/__start_time = world.time;\ - while(!(X)) {;\ - stoplag();\ - if(__start_time + __time <= world.time) {;\ - CRASH("UNTIL_OR_TIMEOUT hit timeout limit of [__time]");\ - };\ - };\ + do { \ + if(__time <= 0) {; \ + CRASH("UNTIL_OR_TIMEOUT given invalid time"); \ + } \ + var/__start_time = world.time; \ + do { \ + if(__start_time + __time <= world.time) {; \ + CRASH("UNTIL_OR_TIMEOUT hit timeout limit of [__time]"); \ + } else { \ + stoplag(); \ + } \ + } while(!(X)) \ } while(FALSE) //Repopulates sortedAreas list diff --git a/code/_globalvars/global_lists.dm b/code/_globalvars/global_lists.dm index 2d07228e8e3d..e663bc287946 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..fd82d4950b91 --- /dev/null +++ b/code/datums/bug_report.dm @@ -0,0 +1,202 @@ +// 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 the bug report author, needed to create the ticket + var/client/initial_user = null + // ckey of the author + var/initial_key = null // just incase they leave after creating the bug report + + /// 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 + initial_key = user.client.key + +/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_key) + 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_key]'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"]] + +## Attached logs +``` +[bug_report_data["log"] ? bug_report_data["log"] : "N/A"] +``` + +## Additional details +- Author: [initial_key] +- Admin: [admin_user] +- Note: [bug_report_data["admin_note"] ? bug_report_data["admin_note"] : "None"] + "} + + 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_OR_TIMEOUT(request.is_complete(), 5 SECONDS) + + var/datum/http_response/response = request.into_response() + if(response.errored || response.status_code != STATUS_SUCCESS) + message_admins(SPAN_ADMINNOTICE("The GitHub API has failed to create the bug report titled [bug_report_data["title"]] approved by [admin_user], status code:[response.status_code]. Please paste this error code into the development channel on discord.")) + external_link_prompt(user) + else + message_admins("[user.ckey] has approved a bug report from [initial_key] 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_key] 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") + if(selected_confirm) // prevent someone from spamming the approve button + to_chat(user, SPAN_WARNING("you have already confirmed the submission, please wait a moment for the API to process your submission.")) + return + 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_key] 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 9bce54ec30d8..69ed0d259384 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..fe0c26035fd6 --- /dev/null +++ b/tgui/packages/tgui/interfaces/BugReportForm.tsx @@ -0,0 +1,217 @@ +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; + admin_note: string; + log: 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 [admin_note, setAdminNote] = useState(report_details?.admin_note || ''); + const [log, setLog] = useState(report_details?.log || ''); + + const submit = () => { + if (!title || !description || !expected_behavior || !steps || !checkBox) { + alert('Please fill out all required fields!'); + return; + } + const updatedReportDetails = { + title, + steps, + description, + expected_behavior, + admin_note, + log, + }; + 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'} +