Skip to content

Commit

Permalink
Adds in-game bug reports without needing a GitHub account (#6392)
Browse files Browse the repository at this point in the history
# 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 <N/A>
Co-authored-by: harryob <[email protected]>
Co-authored-by: Drathek <[email protected]>
  • Loading branch information
3 people committed Jun 19, 2024
1 parent e5a9fe8 commit b9a0286
Show file tree
Hide file tree
Showing 12 changed files with 535 additions and 15 deletions.
1 change: 1 addition & 0 deletions code/__DEFINES/admin.dm
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ GLOBAL_LIST_INIT(note_categories, list("Admin", "Merit", "Whitelist"))
#define OBSERVER_JMP(observer, atom) atom ? "(<a href='?src=[REF(observer)];jumptocoord=1;X=[atom.x];Y=[atom.y];Z=[atom.z]'>JMP</a>)" : ""
#define ARES_MARK(user) "(<a href='?_src_=admin_holder;[HrefToken(forceGlobal = TRUE)];AresMark=[REF(user)]'>MARK</a>)"
#define ARES_REPLY(user, ref) "(<a href='?_src_=admin_holder;[HrefToken(forceGlobal = TRUE)];AresReply=[REF(user)];AresRef=[ref]'>RPLY</a>)"
#define ADMIN_VIEW_BUG_REPORT(datum) "<a href='?_src_=admin_holder;[HrefToken(forceGlobal = TRUE)];view_bug_report=[REF(datum)]'>VIEW REPORT</a>"

/atom/proc/Admin_Coordinates_Readable(area_name, admin_jump_ref)
var/turf/T = get_turf(src)
Expand Down
21 changes: 12 additions & 9 deletions code/__HELPERS/unsorted.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions code/_globalvars/global_lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions code/controllers/configuration/entries/general.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
202 changes: 202 additions & 0 deletions code/datums/bug_report.dm
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions code/modules/admin/topic/topic.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions colonialmarines.dme
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions config/example/config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 3 additions & 6 deletions interface/interface.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading

0 comments on commit b9a0286

Please sign in to comment.