Merge pull request #6679 from VOREStation/upstream-merge-6719

[MIRROR] Adds an In-game Feedback System
This commit is contained in:
Novacat
2020-02-24 21:13:33 -05:00
committed by GitHub
17 changed files with 780 additions and 3 deletions

View File

@@ -0,0 +1,52 @@
GLOBAL_VAR(managed_browser_id_ticker)
// This holds information on managing a /datum/browser object.
// Managing can include things like persisting the state of specific information inside of this object, receiving Topic() calls, or deleting itself when the window is closed.
// This is useful for browser windows to be able to stand 'on their own' instead of being tied to something in the game world, like an object or mob.
/datum/managed_browser
var/client/my_client = null
var/browser_id = null
var/base_browser_id = null
var/title = null
var/size_x = 200
var/size_y = 400
var/display_when_created = TRUE
/datum/managed_browser/New(client/new_client)
if(!new_client)
crash_with("Managed browser object was not given a client.")
return
if(!base_browser_id)
crash_with("Managed browser object does not have a base browser id defined in its type.")
return
my_client = new_client
browser_id = "[base_browser_id]-[GLOB.managed_browser_id_ticker++]"
if(display_when_created)
display()
/datum/managed_browser/Destroy()
my_client = null
return ..()
// Override if you want to have the browser title change conditionally.
// Otherwise it's easier to just change the title variable directly.
/datum/managed_browser/proc/get_title()
return title
// Override to display the html information.
// It is suggested to build it with a list, and use list.Join() at the end.
// This helps prevent excessive concatination, which helps preserves BYOND's string tree from becoming a laggy mess.
/datum/managed_browser/proc/get_html()
return
/datum/managed_browser/proc/display()
interact(get_html(), get_title(), my_client)
/datum/managed_browser/proc/interact(html, title, client/C)
var/datum/browser/popup = new(C.mob, browser_id, title, size_x, size_y, src)
popup.set_content(html)
popup.open()

View File

@@ -0,0 +1,147 @@
/client
var/datum/managed_browser/feedback_form/feedback_form = null
/client/can_vv_get(var_name)
return var_name != NAMEOF(src, feedback_form) // No snooping.
GENERAL_PROTECT_DATUM(datum/managed_browser/feedback_form)
// A fairly simple object to hold information about a player's feedback as it's being written.
// Having this be it's own object instead of being baked into /mob/new_player allows for it to be used
// from other places than just the lobby, and makes it a lot harder for people with dev powers to be naughty with it using VV/proccall.
/datum/managed_browser/feedback_form
base_browser_id = "feedback_form"
title = "Server Feedback"
size_x = 480
size_y = 520
var/feedback_topic = null
var/feedback_body = null
var/feedback_hide_author = FALSE
/datum/managed_browser/feedback_form/New(client/new_client)
feedback_topic = config.sqlite_feedback_topics[1]
..(new_client)
/datum/managed_browser/feedback_form/Destroy()
if(my_client)
my_client.feedback_form = null
return ..()
// Privacy option is allowed if both the config allows it, and the pepper file exists and isn't blank.
/datum/managed_browser/feedback_form/proc/can_be_private()
return config.sqlite_feedback_privacy && SSsqlite.get_feedback_pepper()
/datum/managed_browser/feedback_form/display()
if(!my_client)
return
if(!SSsqlite.can_submit_feedback(my_client))
return
..()
// Builds the window for players to review their feedback.
/datum/managed_browser/feedback_form/get_html()
var/list/dat = list("<html><body>")
dat += "<center>"
dat += "<font size='2'>"
dat += "Here, you can write some feedback for the server.<br>"
dat += "Note that HTML is NOT supported!<br>"
dat += "Click the edit button to begin writing.<br>"
dat += "Your feedback is currently [length(feedback_body)]/[MAX_FEEDBACK_LENGTH] letters long."
dat += "</font>"
dat += "<hr>"
dat += "<h2>Preview</h2></center>"
dat += "Author: "
if(can_be_private())
if(!feedback_hide_author)
dat += "[my_client.ckey] "
dat += span("linkOn", "<b>Visible</b>")
dat += " | "
dat += href(src, list("feedback_hide_author" = 1), "Hashed")
else
dat += "[md5(ckey(lowertext(my_client.ckey + SSsqlite.get_feedback_pepper())))] "
dat += href(src, list("feedback_hide_author" = 0), "Visible")
dat += " | "
dat += span("linkOn", "<b>Hashed</b>")
else
dat += my_client.ckey
dat += "<br>"
if(config.sqlite_feedback_topics.len > 1)
dat += "Topic: [href(src, list("feedback_choose_topic" = 1), feedback_topic)]<br>"
else
dat += "Topic: [config.sqlite_feedback_topics[1]]<br>"
dat += "<br>"
if(feedback_body)
dat += replacetext(feedback_body, "\n", "<br>") // So newlines will look like they work in the preview.
else
dat += "<i>\[Feedback goes here...\]</i>"
dat += "<br>"
dat += href(src, list("feedback_edit_body" = 1), "Edit")
dat += "<hr>"
if(config.sqlite_feedback_cooldown)
dat += "<i>Please note that you will have to wait [config.sqlite_feedback_cooldown] day\s before \
being able to write more feedback after submitting.</i><br>"
dat += href(src, list("feedback_submit" = 1), "Submit")
dat += "</body></html>"
return dat.Join()
/datum/managed_browser/feedback_form/Topic(href, href_list[])
if(!my_client)
return FALSE
if(href_list["feedback_edit_body"])
// This is deliberately not sanitized here, and is instead checked when hitting the submission button,
// as we want to give the user a chance to fix it without needing to rewrite the whole thing.
feedback_body = input(my_client, "Please write your feedback here.", "Feedback Body", feedback_body) as null|message
display() // Refresh the window with new information.
return
if(href_list["feedback_hide_author"])
if(!can_be_private())
feedback_hide_author = FALSE
else
feedback_hide_author = text2num(href_list["feedback_hide_author"])
display()
return
if(href_list["feedback_choose_topic"])
feedback_topic = input(my_client, "Choose the topic you want to submit your feedback under.", "Feedback Topic", feedback_topic) in config.sqlite_feedback_topics
display()
return
if(href_list["feedback_submit"])
// Do some last minute validation, and tell the user if something goes wrong,
// so we don't wipe out their ten thousand page essay due to having a few too many characters.
if(length(feedback_body) > MAX_FEEDBACK_LENGTH)
to_chat(my_client, span("warning", "Your feedback is too long, at [length(feedback_body)] characters, where as the \
limit is [MAX_FEEDBACK_LENGTH]. Please shorten it and try again."))
return
var/text = sanitize(feedback_body, max_length = 0, encode = TRUE, trim = FALSE, extra = FALSE)
if(!text) // No text, or it was super invalid.
to_chat(my_client, span("warning", "It appears you didn't write anything, or it was invalid."))
return
if(alert(my_client, "Are you sure you want to submit your feedback?", "Confirm Submission", "No", "Yes") == "Yes")
var/author_text = my_client.ckey
if(can_be_private() && feedback_hide_author)
author_text = md5(my_client.ckey + SSsqlite.get_feedback_pepper())
var/success = SSsqlite.insert_feedback(author = author_text, topic = feedback_topic, content = feedback_body, sqlite_object = SSsqlite.sqlite_db)
if(!success)
to_chat(my_client, span("warning", "Something went wrong while inserting your feedback into the database. Please try again. \
If this happens again, you should contact a developer."))
return
my_client.mob << browse(null, "window=[browser_id]") // Closes the window.
if(istype(my_client.mob, /mob/new_player))
var/mob/new_player/NP = my_client.mob
NP.new_player_panel_proc() // So the feedback button goes away, if the user gets put on cooldown.
qdel(src)

View File

@@ -0,0 +1,162 @@
/client
var/datum/managed_browser/feedback_viewer/feedback_viewer = null
/datum/admins/proc/view_feedback()
set category = "Admin"
set name = "View Feedback"
set desc = "Open the Feedback Viewer"
if(!check_rights(R_ADMIN))
return
if(usr.client.feedback_viewer)
usr.client.feedback_viewer.display()
else
usr.client.feedback_viewer = new(usr.client)
// This object holds the code to run the admin feedback viewer.
/datum/managed_browser/feedback_viewer
base_browser_id = "feedback_viewer"
title = "Submitted Feedback"
size_x = 900
size_y = 500
var/database/query/last_query = null
/datum/managed_browser/feedback_viewer/New(client/new_client)
if(!check_rights(R_ADMIN, new_client)) // Just in case someone figures out a way to spawn this as non-staff.
message_admins("[new_client] tried to view feedback with insufficent permissions.")
qdel(src)
..()
/datum/managed_browser/feedback_viewer/Destroy()
if(my_client)
my_client.feedback_viewer = null
return ..()
/datum/managed_browser/feedback_viewer/proc/feedback_filter(row_name, thing_to_find, exact = FALSE)
var/database/query/query = null
if(exact) // Useful for ID searches, so searching for 'id 10' doesn't also get 'id 101'.
query = new({"
SELECT *
FROM [SQLITE_TABLE_FEEDBACK]
WHERE [row_name] == ?
ORDER BY [SQLITE_FEEDBACK_COLUMN_ID]
DESC LIMIT 50;
"},
thing_to_find
)
else
// Wrap the thing in %s so LIKE will work.
thing_to_find = "%[thing_to_find]%"
query = new({"
SELECT *
FROM [SQLITE_TABLE_FEEDBACK]
WHERE [row_name] LIKE ?
ORDER BY [SQLITE_FEEDBACK_COLUMN_ID]
DESC LIMIT 50;
"},
thing_to_find
)
query.Execute(SSsqlite.sqlite_db)
SSsqlite.sqlite_check_for_errors(query, "Admin Feedback Viewer - Filter by [row_name] to find [thing_to_find]")
return query
// Builds the window for players to review their feedback.
/datum/managed_browser/feedback_viewer/get_html()
var/list/dat = list("<html><body>")
if(!last_query) // If no query was done before, just show the most recent feedbacks.
var/database/query/query = new({"
SELECT *
FROM [SQLITE_TABLE_FEEDBACK]
ORDER BY [SQLITE_FEEDBACK_COLUMN_ID]
DESC LIMIT 50;
"}
)
query.Execute(SSsqlite.sqlite_db)
SSsqlite.sqlite_check_for_errors(query, "Admin Feedback Viewer")
last_query = query
dat += "<table border='1' style='width:100%'>"
dat += "<tr>"
dat += "<th>[href(src, list("filter_id" = 1), "ID")]</th>"
dat += "<th>[href(src, list("filter_topic" = 1), "Topic")]</th>"
dat += "<th>[href(src, list("filter_author" = 1), "Author")]</th>"
dat += "<th>[href(src, list("filter_content" = 1), "Content")]</th>"
dat += "<th>[href(src, list("filter_datetime" = 1), "Datetime")]</th>"
dat += "</tr>"
while(last_query.NextRow())
var/list/row_data = last_query.GetRowData()
dat += "<tr>"
dat += "<td>[row_data[SQLITE_FEEDBACK_COLUMN_ID]]</td>"
dat += "<td>[row_data[SQLITE_FEEDBACK_COLUMN_TOPIC]]</td>"
dat += "<td>[row_data[SQLITE_FEEDBACK_COLUMN_AUTHOR]]</td>" // TODO: Color this to make hashed keys more distinguishable.
var/text = row_data[SQLITE_FEEDBACK_COLUMN_CONTENT]
if(length(text) > 512)
text = href(src, list(
"show_full_feedback" = 1,
"feedback_author" = row_data[SQLITE_FEEDBACK_COLUMN_AUTHOR],
"feedback_content" = row_data[SQLITE_FEEDBACK_COLUMN_CONTENT]
), "[copytext(text, 1, 64)]... ([length(text)])")
else
text = replacetext(text, "\n", "<br>")
dat += "<td>[text]</td>"
dat += "<td>[row_data[SQLITE_FEEDBACK_COLUMN_DATETIME]]</td>"
dat += "</tr>"
dat += "</table>"
dat += "</body></html>"
return dat.Join()
// Used to show the full version of feedback in a seperate window.
/datum/managed_browser/feedback_viewer/proc/display_big_feedback(author, text)
var/list/dat = list("<html><body>")
dat += replacetext(text, "\n", "<br>")
var/datum/browser/popup = new(my_client.mob, "feedback_big", "[author]'s Feedback", 480, 520, src)
popup.set_content(dat.Join())
popup.open()
/datum/managed_browser/feedback_viewer/Topic(href, href_list[])
if(!my_client)
return FALSE
if(href_list["close"]) // To avoid refreshing.
return
if(href_list["show_full_feedback"])
display_big_feedback(href_list["feedback_author"], href_list["feedback_content"])
return
if(href_list["filter_id"])
var/id_to_search = input(my_client, "Write feedback ID here.", "Filter by ID", null) as null|num
if(id_to_search)
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_ID, id_to_search, TRUE)
if(href_list["filter_author"])
var/author_to_search = input(my_client, "Write desired key or hash here. Partial keys/hashes are allowed.", "Filter by Author", null) as null|text
if(author_to_search)
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_AUTHOR, author_to_search)
if(href_list["filter_topic"])
var/topic_to_search = input(my_client, "Write desired topic here. Partial topics are allowed. \
\nThe current topics in the config are [english_list(config.sqlite_feedback_topics)].", "Filter by Topic", null) as null|text
if(topic_to_search)
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_TOPIC, topic_to_search)
if(href_list["filter_content"])
var/content_to_search = input(my_client, "Write desired content to find here. Partial matches are allowed.", "Filter by Content", null) as null|message
if(content_to_search)
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_CONTENT, content_to_search)
if(href_list["filter_datetime"])
var/datetime_to_search = input(my_client, "Write desired datetime. Partial matches are allowed.\n\
Format is 'YYYY-MM-DD HH:MM:SS'.", "Filter by Datetime", null) as null|text
if(datetime_to_search)
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_DATETIME, datetime_to_search)
// Refresh.
display()