diff --git a/code/__defines/_protect.dm b/code/__defines/_protect.dm new file mode 100644 index 0000000000..b10a6264bd --- /dev/null +++ b/code/__defines/_protect.dm @@ -0,0 +1,11 @@ +///Protects a datum from being VV'd +#define GENERAL_PROTECT_DATUM(Path)\ +##Path/can_vv_get(var_name){\ + return FALSE;\ +}\ +##Path/vv_edit_var(var_name, var_value){\ + return FALSE;\ +}\ +##Path/CanProcCall(procname){\ + return FALSE;\ +} \ No newline at end of file diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index e8242af113..8fcabca714 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -113,6 +113,7 @@ #define MAX_RECORD_LENGTH 24576 #define MAX_LNAME_LEN 64 #define MAX_NAME_LEN 52 +#define MAX_FEEDBACK_LENGTH 4096 #define MAX_TEXTFILE_LENGTH 128000 // 512GQ file // Event defines. diff --git a/code/__defines/sqlite_defines.dm b/code/__defines/sqlite_defines.dm new file mode 100644 index 0000000000..8cda3b62a0 --- /dev/null +++ b/code/__defines/sqlite_defines.dm @@ -0,0 +1,7 @@ +#define SQLITE_TABLE_FEEDBACK "feedback" + +#define SQLITE_FEEDBACK_COLUMN_ID "id" +#define SQLITE_FEEDBACK_COLUMN_AUTHOR "author" +#define SQLITE_FEEDBACK_COLUMN_TOPIC "topic" +#define SQLITE_FEEDBACK_COLUMN_CONTENT "content" +#define SQLITE_FEEDBACK_COLUMN_DATETIME "datetime" \ No newline at end of file diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index fe01b602fc..58552c6905 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -52,6 +52,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G // Subsystem init_order, from highest priority to lowest priority // Subsystems shutdown in the reverse of the order they initialize in // The numbers just define the ordering, they are meaningless otherwise. +#define INIT_ORDER_SQLITE 19 #define INIT_ORDER_CHEMISTRY 18 #define INIT_ORDER_MAPPING 17 #define INIT_ORDER_DECALS 16 diff --git a/code/_helpers/unsorted.dm b/code/_helpers/unsorted.dm index a716f3c7b4..607c52e166 100644 --- a/code/_helpers/unsorted.dm +++ b/code/_helpers/unsorted.dm @@ -1577,3 +1577,11 @@ var/mob/dview/dview_mob = new else return "\[[url_encode(thing.tag)]\]" return "\ref[input]" + +// Painlessly creates an element. +// First argument is where to send the Topic call to when clicked. Should be a reference to an object. This is generally src, but not always. +// Second one is for all the params that will be sent. Uses an assoc list (e.g. "value" = "5"). +// Note that object refs will be converted to text, as if \ref[thing] was done. To get the ref back on Topic() side, you will need to use locate(). +// Third one is the text that will be clickable. +/proc/href(href_src, list/href_params, href_text) + return "[href_text]" \ No newline at end of file diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index 36b676b3a0..2af8467b98 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -252,6 +252,17 @@ var/list/gamemode_cache = list() var/random_submap_orientation = FALSE // If true, submaps loaded automatically can be rotated. var/autostart_solars = FALSE // If true, specifically mapped in solar control computers will set themselves up when the round starts. + // New shiny SQLite stuff. + // The basics. + var/sqlite_enabled = FALSE // If it should even be active. SQLite can be ran alongside other databases but you should not have them do the same functions. + + // In-Game Feedback. + var/sqlite_feedback = FALSE // Feedback cannot be submitted if this is false. + var/list/sqlite_feedback_topics = list("General") // A list of 'topics' that feedback can be catagorized under by the submitter. + var/sqlite_feedback_privacy = FALSE // If true, feedback submitted can have its author name be obfuscated. This is not 100% foolproof (it's md5 ffs) but can stop casual snooping. + var/sqlite_feedback_cooldown = 0 // How long one must wait, in days, to submit another feedback form. Used to help prevent spam, especially with privacy active. 0 = No limit. + var/sqlite_feedback_min_age = 0 // Used to block new people from giving feedback. This metric is very bad but it can help slow down spammers. + /datum/configuration/New() var/list/L = typesof(/datum/game_mode) - /datum/game_mode for (var/T in L) @@ -841,6 +852,23 @@ var/list/gamemode_cache = list() if("autostart_solars") config.autostart_solars = TRUE + if("sqlite_enabled") + config.sqlite_enabled = TRUE + + if("sqlite_feedback") + config.sqlite_feedback = TRUE + + if("sqlite_feedback_topics") + config.sqlite_feedback_topics = splittext(value, ";") + if(!config.sqlite_feedback_topics.len) + config.sqlite_feedback_topics += "General" + + if("sqlite_feedback_privacy") + config.sqlite_feedback_privacy = TRUE + + if("sqlite_feedback_cooldown") + config.sqlite_feedback_cooldown = text2num(value) + else diff --git a/code/controllers/subsystems/sqlite.dm b/code/controllers/subsystems/sqlite.dm new file mode 100644 index 0000000000..3d18ec4a7a --- /dev/null +++ b/code/controllers/subsystems/sqlite.dm @@ -0,0 +1,187 @@ +// This holds all the code needed to manage and use a SQLite database. +// It is merely a file sitting inside the data directory, as opposed to a full fledged DB service, +// however this makes it a lot easier to test, and it is natively supported by BYOND. +SUBSYSTEM_DEF(sqlite) + name = "SQLite" + init_order = INIT_ORDER_SQLITE + flags = SS_NO_FIRE + var/database/sqlite_db = null + +/datum/controller/subsystem/sqlite/Initialize(timeofday) + connect() + if(sqlite_db) + init_schema(sqlite_db) + return ..() + +/datum/controller/subsystem/sqlite/proc/connect() + if(!config.sqlite_enabled) + return + + if(!sqlite_db) + sqlite_db = new("data/sqlite/sqlite.db") // The path has to be hardcoded or BYOND silently fails. + + + if(!sqlite_db) + to_world_log("Failed to load or create a SQLite database.") + log_debug("ERROR: SQLite database is active in config but failed to load.") + else + to_world_log("Sqlite database connected.") + +// Makes the tables, if they do not already exist in the sqlite file. +/datum/controller/subsystem/sqlite/proc/init_schema(database/sqlite_object) + // Feedback table. + // Note that this is for direct feedback from players using the in-game feedback system and NOT for stat tracking. + // Player ckeys are not stored in this table as a unique key due to a config option to hash the keys to encourage more honest feedback. + /* + * id - Primary unique key to ID a specific piece of feedback. + NOT used to id people submitting feedback. + * author - The person who submitted it. Will be the ckey, or a hash of the ckey, + if both the config supports it, and the user wants it. + * topic - A specific category to organize feedback under. Options are defined in the config file. + * content - What the author decided to write to the staff. Limited to MAX_FEEDBACK_LENGTH. + * datetime - When the author submitted their feedback, acts as a timestamp. + */ + var/database/query/init_schema = new( + {" + CREATE TABLE IF NOT EXISTS [SQLITE_TABLE_FEEDBACK] + ( + `[SQLITE_FEEDBACK_COLUMN_ID]` INTEGER NOT NULL UNIQUE, + `[SQLITE_FEEDBACK_COLUMN_AUTHOR]` TEXT NOT NULL, + `[SQLITE_FEEDBACK_COLUMN_TOPIC]` TEXT NOT NULL, + `[SQLITE_FEEDBACK_COLUMN_CONTENT]` TEXT NOT NULL, + `[SQLITE_FEEDBACK_COLUMN_DATETIME]` TEXT NOT NULL, + PRIMARY KEY(`[SQLITE_FEEDBACK_COLUMN_ID]`) + ); + "} + ) + init_schema.Execute(sqlite_object) + sqlite_check_for_errors(init_schema, "Feedback table creation") + + // Add more schemas below this if the SQLite DB gets expanded for things like persistant news, polls, bans, deaths, etc. + +// General error checking for SQLite. +// Returns true if something went wrong. Also writes a log. +// The desc parameter should be unique for each call, to make it easier to track down where the error occured. +/datum/controller/subsystem/sqlite/proc/sqlite_check_for_errors(var/database/query/query_used, var/desc) + if(query_used && query_used.ErrorMsg()) + log_debug("SQLite Error: [desc] : [query_used.ErrorMsg()]") + return TRUE + return FALSE + + +/************ + * Feedback * + ************/ + +// Inserts data into the feedback table in a painless manner. +// Returns TRUE if no issues happened, FALSE otherwise. +/datum/controller/subsystem/sqlite/proc/insert_feedback(author, topic, content, database/sqlite_object) + if(!author || !topic || !content) + CRASH("One or more parameters was invalid.") + + // Sanitize everything to avoid sneaky stuff. + var/sqlite_author = sql_sanitize_text(ckey(lowertext(author))) + var/sqlite_content = sql_sanitize_text(content) + var/sqlite_topic = sql_sanitize_text(topic) + + var/database/query/query = new( + "INSERT INTO [SQLITE_TABLE_FEEDBACK] (\ + [SQLITE_FEEDBACK_COLUMN_AUTHOR], \ + [SQLITE_FEEDBACK_COLUMN_TOPIC], \ + [SQLITE_FEEDBACK_COLUMN_CONTENT], \ + [SQLITE_FEEDBACK_COLUMN_DATETIME]) \ + \ + VALUES (\ + ?,\ + ?,\ + ?,\ + datetime('now'))", + sqlite_author, + sqlite_topic, + sqlite_content + ) + query.Execute(sqlite_object) + return !sqlite_check_for_errors(query, "Insert Feedback") + +/datum/controller/subsystem/sqlite/proc/can_submit_feedback(client/C) + if(!config.sqlite_enabled) + return FALSE + if(config.sqlite_feedback_min_age && !is_old_enough(C)) + return FALSE + if(config.sqlite_feedback_cooldown > 0 && get_feedback_cooldown(C.key, config.sqlite_feedback_cooldown, sqlite_db) > 0) + return FALSE + return TRUE + +// Returns TRUE if the player is 'old' enough, according to the config. +/datum/controller/subsystem/sqlite/proc/is_old_enough(client/C) + if(get_player_age(C.key) < config.sqlite_feedback_min_age) + return FALSE + return TRUE + + +// Returns how many days someone has to wait, to submit more feedback, or 0 if they can do so right now. +/datum/controller/subsystem/sqlite/proc/get_feedback_cooldown(player_ckey, cooldown, database/sqlite_object) + player_ckey = sql_sanitize_text(ckey(lowertext(player_ckey))) + var/potential_hashed_ckey = sql_sanitize_text(md5(player_ckey + SSsqlite.get_feedback_pepper())) + + // First query is to get the most recent time the player has submitted feedback. + var/database/query/query = new({" + SELECT [SQLITE_FEEDBACK_COLUMN_DATETIME] + FROM [SQLITE_TABLE_FEEDBACK] + WHERE [SQLITE_FEEDBACK_COLUMN_AUTHOR] == ? OR [SQLITE_FEEDBACK_COLUMN_AUTHOR] == ? + ORDER BY [SQLITE_FEEDBACK_COLUMN_DATETIME] + DESC LIMIT 1; + "}, + player_ckey, + potential_hashed_ckey + ) + query.Execute(sqlite_object) + sqlite_check_for_errors(query, "Rate Limited Check 1") + + // It is possible this is their first time, so there won't be a next row. + if(query.NextRow()) // If this is true, the user has submitted feedback at least once. + var/list/row_data = query.GetRowData() + var/last_submission_datetime = row_data[SQLITE_FEEDBACK_COLUMN_DATETIME] + + // Now we have the datetime, we need to do something to compare it. + // Second query is to calculate the difference between two datetimes. + // This is done on the SQLite side because parsing datetimes with BYOND is probably a bad idea. + query = new( + "SELECT julianday('now') - julianday(?) \ + AS 'datediff';", + last_submission_datetime + ) + query.Execute(sqlite_object) + sqlite_check_for_errors(query, "Rate Limited Check 2") + + query.NextRow() + row_data = query.GetRowData() + var/date_diff = row_data["datediff"] + + // Now check if it's too soon to give more feedback. + if(text2num(date_diff) < cooldown) // Too soon. + return round(cooldown - date_diff, 0.1) + return 0.0 + + +// A Pepper is like a Salt but only one exists and is supposed to be outside of a database. +// If the file is properly protected, it can only be viewed/copied by sys-admins generating a log, which is much more conspicious than accessing/copying a DB. +// This stops mods/admins/etc from guessing the author by shoving names in an MD5 hasher until they pick the right one. +// Don't use this for things needing actual security. +/datum/controller/subsystem/sqlite/proc/get_feedback_pepper() + var/pepper_file = file2list("config/sqlite_feedback_pepper.txt") + var/pepper = null + for(var/line in pepper_file) + if(!line) + continue + if(length(line) == 0) + continue + else if(copytext(line, 1, 2) == "#") + continue + else + pepper = line + break + return pepper + +/datum/controller/subsystem/sqlite/CanProcCall(procname) + return procname != "get_feedback_pepper" diff --git a/code/datums/managed_browsers/_managed_browser.dm b/code/datums/managed_browsers/_managed_browser.dm new file mode 100644 index 0000000000..c915c7e86a --- /dev/null +++ b/code/datums/managed_browsers/_managed_browser.dm @@ -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() \ No newline at end of file diff --git a/code/datums/managed_browsers/feedback_form.dm b/code/datums/managed_browsers/feedback_form.dm new file mode 100644 index 0000000000..bc5c456aec --- /dev/null +++ b/code/datums/managed_browsers/feedback_form.dm @@ -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("
") + dat += "| [href(src, list("filter_id" = 1), "ID")] | " + dat += "[href(src, list("filter_topic" = 1), "Topic")] | " + dat += "[href(src, list("filter_author" = 1), "Author")] | " + dat += "[href(src, list("filter_content" = 1), "Content")] | " + dat += "[href(src, list("filter_datetime" = 1), "Datetime")] | " + dat += "
|---|---|---|---|---|
| [row_data[SQLITE_FEEDBACK_COLUMN_ID]] | " + dat += "[row_data[SQLITE_FEEDBACK_COLUMN_TOPIC]] | " + dat += "[row_data[SQLITE_FEEDBACK_COLUMN_AUTHOR]] | " // 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", "[text] | " + dat += "[row_data[SQLITE_FEEDBACK_COLUMN_DATETIME]] | " + dat += "
[href(src, list("give_feedback" = 1), "Give Feedback")]
" output += "" - panel = new(src, "Welcome","Welcome", 210, 280, src) + panel = new(src, "Welcome","Welcome", 210, 300, src) panel.set_window_options("can_close=0") panel.set_content(output) panel.open() @@ -170,7 +172,10 @@ if(href_list["SelectedJob"]) +<<<<<<< HEAD /* Vorestation Removal Start +======= +>>>>>>> 97f8c1d... Merge pull request #6719 from Neerti/feedback_system //Prevents people rejoining as same character. for (var/mob/living/carbon/human/C in mob_list) var/char_name = client.prefs.real_name @@ -306,6 +311,15 @@ show_hidden_jobs = !show_hidden_jobs LateChoices() + if(href_list["give_feedback"]) + if(!SSsqlite.can_submit_feedback(my_client)) + return + + if(client.feedback_form) + client.feedback_form.display() // In case they closed the form early. + else + client.feedback_form = new(client) + /mob/new_player/proc/handle_server_news() if(!client) return @@ -325,6 +339,7 @@ popup.set_content(dat) popup.open() + /mob/new_player/proc/IsJobAvailable(rank) var/datum/job/job = job_master.GetJob(rank) if(!job) return 0 diff --git a/code/unit_tests/sqlite_tests.dm b/code/unit_tests/sqlite_tests.dm new file mode 100644 index 0000000000..8263f58064 --- /dev/null +++ b/code/unit_tests/sqlite_tests.dm @@ -0,0 +1,69 @@ +/datum/unit_test/sqlite + name = "SQLite template" // Template has to be in the name or this test will be ran, and fail. + var/database/stub_sqlite_db = null + +/datum/unit_test/sqlite/proc/setup_stub_db() + fdel("data/sqlite/testing_[name].db") // In case any remain from a previous local test, so we can have a clean new database. + stub_sqlite_db = new("data/sqlite/testing_[name].db") // Unfortunately, byond doesn't like having working sqlite stuff w/o a file existing. + SSsqlite.init_schema(stub_sqlite_db) + +// Feedback table tests. +/datum/unit_test/sqlite/feedback/insert + name = "SQLITE FEEDBACK: Insert and Retrieve Data" + +/datum/unit_test/sqlite/feedback/insert/start_test() + // Arrange. + setup_stub_db() + var/test_author = "alice" + var/test_topic = "Test" + var/test_content = "Bob is lame." + + // Act. + SSsqlite.insert_feedback(author = test_author, topic = test_topic, content = test_content, sqlite_object = stub_sqlite_db) + var/database/query/Q = new("SELECT * FROM [SQLITE_TABLE_FEEDBACK]") + Q.Execute(stub_sqlite_db) + SSsqlite.sqlite_check_for_errors(Q, "Sqlite Insert Unit Test") + Q.NextRow() + + // Assert. + var/list/row_data = Q.GetRowData() + if(row_data[SQLITE_FEEDBACK_COLUMN_AUTHOR] == test_author && row_data[SQLITE_FEEDBACK_COLUMN_TOPIC] == test_topic && row_data[SQLITE_FEEDBACK_COLUMN_CONTENT] == test_content) + pass("No issues found.") + else + fail("Data insert and loading failed to have matching information.") + return TRUE + + +/datum/unit_test/sqlite/feedback/cooldown + name = "SQLITE FEEDBACK: Cooldown" + +/datum/unit_test/sqlite/feedback/cooldown/start_test() + // Arrange. + setup_stub_db() + var/days_to_wait = 1 + var/issues = 0 + + // Act. + SSsqlite.insert_feedback(author = "Alice", topic = "Testing", content = "This is a test.", sqlite_object = stub_sqlite_db) + + var/alice_cooldown_block = SSsqlite.get_feedback_cooldown("Alice", days_to_wait, stub_sqlite_db) + var/bob_cooldown = SSsqlite.get_feedback_cooldown("Bob", days_to_wait, stub_sqlite_db) + days_to_wait = 0 + var/alice_cooldown_allow = SSsqlite.get_feedback_cooldown("Alice", days_to_wait, stub_sqlite_db) + + // Assert. + if(alice_cooldown_block <= 0) + issues++ + log_unit_test("User 'Alice' did not receive a cooldown, when they were supposed to.") + if(bob_cooldown > 0) + issues++ + log_unit_test("User 'Bob' did receive a cooldown, when they did not do anything.") + if(alice_cooldown_allow > 0) + issues++ + log_unit_test("User 'Alice' did receive a cooldown, when no cooldown is supposed to be enforced.") + + if(issues) + fail("[issues] issues were found.") + else + pass("No issues found.") + return TRUE diff --git a/config/example/config.txt b/config/example/config.txt index 8853f337cc..f9be5fc872 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -485,4 +485,41 @@ IPR_BLOCK_BAD_IPS IPR_ALLOW_EXISTING # And what that age is (number) -IPR_MINIMUM_AGE 5 \ No newline at end of file +IPR_MINIMUM_AGE 5 + + +## -SQLite Options- +## Uncomment to enable the use of SQLite. This does nothing by itself but other features that require SQLite will need this to be on. +## This can safely run alongside a MySQL/MariaDB database if they are powering seperate features. +# SQLITE_ENABLED + +## Uncomment to enable a SQLite-powered in-game feedback system. +## SQLite must be enabled for this to function. +## It offers a means for players to be able to give feedback about the server. +## The benefit of doing so in-game is that the quality of feedback received will likely be superior, as it self-selects for people who care enough to join the game. +# SQLITE_FEEDBACK + +## A list of 'topics' that can be used to categorize feedback submitted, chosen +## by the user. Have each topic seperated by a ';', as seen below. +## The first one in the list will be the default one used, if the user does not change it. +# SQLITE_FEEDBACK_TOPICS General; Suggestion; Complaint + +## Uncomment to add a layer of privacy to player feedback, by hashing their key, if the user wants to. +## This is intended to encourage more honest feedback, while still allowing the ability to determine +## if its just one person submitting everything. +## A 'pepper.txt' containing a secret string must exist in the /config folder. +## If this is turned off, users won't have the option to obfuscate their key. +## Note that changing this does not retroactively change past submissions. +# SQLITE_FEEDBACK_PRIVACY + +## Determines the 'cooldown' inbetween submissions, in days. +## This is recommended if privacy is active, to prevent spam floods. +## Less needed if feedback is not anonymous, since you can just ban spammers. +## Setting to zero means no rate limiting. +SQLITE_FEEDBACK_COOLDOWN 0 + +## Determines if feedback should be restricted based on how recently someone first joined. +## This is very unreliable due to how the age system works in general, but it might still be helpful. +## Set this to how many days you want someone to have to wait when they first join. +## Setting to zero will disable this restriction. +SQLITE_FEEDBACK_MIN_AGE 7 diff --git a/config/example/sqlite_feedback_pepper.txt b/config/example/sqlite_feedback_pepper.txt new file mode 100644 index 0000000000..8986d2714e --- /dev/null +++ b/config/example/sqlite_feedback_pepper.txt @@ -0,0 +1,11 @@ +# This is used to alter the hash for in-game feedback authors. +# The reason for this is that doing this stops admins trying to +# guess who submitted a specific piece of feedback by feeding +# a list of ckeys into an MD5 hasher. +# In order for this to work, this file must contain a string, to be +# added onto the ckey before hashing, and ideally as few people as +# possible should know the string. It is also recommended to utilize +# file permissions (e.g. chown) to only allow the game to access +# this file, and forbid people with access to the server directly +# from just opening the file (even if they use sudo, it'll make a log). +# Make sure the string doesn't start with a #, or it'll be commented out. diff --git a/html/changelogs/neerti-feedback_system.yml b/html/changelogs/neerti-feedback_system.yml new file mode 100644 index 0000000000..1dd7a2b3a3 --- /dev/null +++ b/html/changelogs/neerti-feedback_system.yml @@ -0,0 +1,36 @@ +################################ +# Example Changelog File +# +# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb. +# +# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.) +# When it is, any changes listed below will disappear. +# +# Valid Prefixes: +# bugfix +# wip (For works in progress) +# tweak +# soundadd +# sounddel +# rscadd (general adding of nice things) +# rscdel (general deleting of nice things) +# imageadd +# imagedel +# maptweak +# spellcheck (typo fixes) +# experiment +################################# + +# Your name. +author: Neerti + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries. +# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog. +changes: + - rscadd: "Adds an in-game feedback system that can be activated in the server configuration. Can be accessed from the lobby, with a button next to the other lobby buttons. Additional features and restrictions on usage are controlled by the server configuration." diff --git a/vorestation.dme b/vorestation.dme index 366937ae38..cd503b76bd 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -27,7 +27,11 @@ #include "code\__defines\_compile_options.dm" #include "code\__defines\_lists.dm" #include "code\__defines\_planes+layers.dm" +<<<<<<< HEAD:vorestation.dme #include "code\__defines\_planes+layers_vr.dm" +======= +#include "code\__defines\_protect.dm" +>>>>>>> 97f8c1d... Merge pull request #6719 from Neerti/feedback_system:polaris.dme #include "code\__defines\_tick.dm" #include "code\__defines\admin.dm" #include "code\__defines\admin_vr.dm" @@ -68,7 +72,11 @@ #include "code\__defines\roguemining_vr.dm" #include "code\__defines\sound.dm" #include "code\__defines\species_languages.dm" +<<<<<<< HEAD:vorestation.dme #include "code\__defines\species_languages_vr.dm" +======= +#include "code\__defines\sqlite_defines.dm" +>>>>>>> 97f8c1d... Merge pull request #6719 from Neerti/feedback_system:polaris.dme #include "code\__defines\stat_tracking.dm" #include "code\__defines\subsystems.dm" #include "code\__defines\subsystems_vr.dm" @@ -246,6 +254,7 @@ #include "code\controllers\subsystems\planets.dm" #include "code\controllers\subsystems\radiation.dm" #include "code\controllers\subsystems\shuttles.dm" +#include "code\controllers\subsystems\sqlite.dm" #include "code\controllers\subsystems\sun.dm" #include "code\controllers\subsystems\time_track.dm" #include "code\controllers\subsystems\timer.dm" @@ -320,6 +329,9 @@ #include "code\datums\looping_sounds\machinery_sounds.dm" #include "code\datums\looping_sounds\sequence.dm" #include "code\datums\looping_sounds\weather_sounds.dm" +#include "code\datums\managed_browsers\_managed_browser.dm" +#include "code\datums\managed_browsers\feedback_form.dm" +#include "code\datums\managed_browsers\feedback_viewer.dm" #include "code\datums\observation\_debug.dm" #include "code\datums\observation\_defines.dm" #include "code\datums\observation\destroyed.dm" @@ -3373,6 +3385,7 @@ #include "code\unit_tests\map_tests.dm" #include "code\unit_tests\mob_tests.dm" #include "code\unit_tests\research_tests.dm" +#include "code\unit_tests\sqlite_tests.dm" #include "code\unit_tests\unit_test.dm" #include "code\unit_tests\unit_test_vr.dm" #include "code\unit_tests\vore_tests_vr.dm"