diff --git a/SQL/database_changelog.md b/SQL/database_changelog.md index 7826846980f..ae588adf65d 100644 --- a/SQL/database_changelog.md +++ b/SQL/database_changelog.md @@ -2,15 +2,15 @@ Any time you make a change to the schema files, remember to increment the databa Make sure to also update `DB_MAJOR_VERSION` and `DB_MINOR_VERSION`, which can be found in `code/__DEFINES/subsystem.dm`. -The latest database version is 5.26 (5.24 for /tg/); The query to update the schema revision table is: +The latest database version is 5.25 (5.24 for /tg/); The query to update the schema revision table is: ```sql -INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 26); +INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 25); ``` or ```sql -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 26); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 25); ``` In any query remember to add a prefix to the table names if you use one. diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index ed6ebb3c992..ecc615880e8 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -276,10 +276,10 @@ CREATE TABLE `library_action` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `book` int(10) unsigned NOT NULL, `reason` longtext DEFAULT NULL, - `ckey` varchar(11) NOT NULL DEFAULT '', + `ckey` varchar(32) NOT NULL DEFAULT '', `datetime` datetime NOT NULL DEFAULT current_timestamp(), `action` varchar(11) NOT NULL DEFAULT '', - `ip_addr` int(11) NOT NULL, + `ip_addr` int(10) unsigned NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql index e69cd9ee046..659162158ec 100644 --- a/SQL/tgstation_schema_prefixed.sql +++ b/SQL/tgstation_schema_prefixed.sql @@ -275,10 +275,10 @@ CREATE TABLE `SS13_library_action` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `book` int(10) unsigned NOT NULL, `reason` longtext DEFAULT NULL, - `ckey` varchar(11) NOT NULL DEFAULT '', + `ckey` varchar(32) NOT NULL DEFAULT '', `datetime` datetime NOT NULL DEFAULT current_timestamp(), `action` varchar(11) NOT NULL DEFAULT '', - `ip_addr` int(11) NOT NULL, + `ip_addr` int(10) unsigned NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/code/__DEFINES/library.dm b/code/__DEFINES/library.dm new file mode 100644 index 00000000000..0c84434578d --- /dev/null +++ b/code/__DEFINES/library.dm @@ -0,0 +1,9 @@ +#define DEFAULT_UPLOAD_CATAGORY "Fiction" +#define DEFAULT_SEARCH_CATAGORY "Any" + +///How many books should we load per page? +#define BOOKS_PER_PAGE 18 +///How many checkout records should we load per page? +#define CHECKOUTS_PER_PAGE 17 +///How many inventory items should we load per page? +#define INVENTORY_PER_PAGE 19 diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 9662f38c498..cdc7a29c5e3 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -107,7 +107,7 @@ GLOBAL_PROTECT(admin_verbs_admin) /client/proc/view_opfors, /*OPFOR - SKYRAT EDIT ADDITION*/ /client/proc/lorecaster_story_manager, /* SKYRAT EDIT ADDITION */ ) -GLOBAL_LIST_INIT(admin_verbs_ban, list(/client/proc/unban_panel, /client/proc/ban_panel, /client/proc/stickybanpanel)) +GLOBAL_LIST_INIT(admin_verbs_ban, list(/client/proc/unban_panel, /client/proc/ban_panel, /client/proc/stickybanpanel, /client/proc/library_control)) GLOBAL_PROTECT(admin_verbs_ban) GLOBAL_LIST_INIT(admin_verbs_sounds, list(/client/proc/play_local_sound, /client/proc/play_direct_mob_sound, /client/proc/play_sound, /client/proc/set_round_end_sound)) GLOBAL_PROTECT(admin_verbs_sounds) @@ -1060,3 +1060,14 @@ GLOBAL_PROTECT(admin_verbs_poll) message_admins("[key_name_admin(usr)] has loaded lazy template '[choice]'") to_chat(usr, span_boldnicegreen("Template loaded, you have been moved to the bottom left of the reservation.")) + +/client/proc/library_control() + set name = "Library Management" + set category = "Admin" + if(!check_rights(R_BAN)) + return + + if(!holder.library_manager) + holder.library_manager = new() + holder.library_manager.ui_interact(usr) + SSblackbox.record_feedback("tally", "admin_verb", 1, "Library Management") // If you are copy-pasting this, ensure the 4th parameter is unique to the new proc! diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index bbdb6bfbe22..8cb57ff8f83 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -35,6 +35,7 @@ GLOBAL_PROTECT(href_token) var/datum/particle_editor/particle_test var/datum/colorblind_tester/color_test = new var/datum/plane_master_debug/plane_debug + var/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/library_manager /// Whether or not the user tried to connect, but was blocked by 2FA var/blocked_by_2fa = FALSE diff --git a/code/modules/library/admin_only.dm b/code/modules/library/admin_only.dm new file mode 100644 index 00000000000..3e10617d9fe --- /dev/null +++ b/code/modules/library/admin_only.dm @@ -0,0 +1,367 @@ +#define BOOK_ADMIN_DELETE "deleted" +#define BOOK_ADMIN_RESTORE "undeleted" +#define BOOK_ADMIN_REPORT "reported" + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker + interface_type = "LibraryAdmin" + /// When a user clicks view, do we display the raw text, or process it with markdown + var/view_raw = FALSE + /// If we should show deleted entries or not + var/show_deleted = TRUE + /// The current ckey we're looking for + var/ckey = "" + /// List mapping requested book ids to a list of their edit logs + var/list/book_history = list() + + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/can_db_request() + if(sending_request) + return FALSE + return TRUE + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/hash_search_info() + . = ..() + return "[.]-[ckey]-[show_deleted]" + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/update_page_contents() + if(sending_request) //Final defense against nerds spamming db requests + return + sending_request = TRUE + search_page = clamp(search_page, 0, page_count) + var/datum/db_query/query_library_list_books = SSdbcore.NewQuery({" + SELECT id, author, title, category, ckey, deleted + FROM [format_table_name("library")] + [show_deleted ? "" : "WHERE deleted IS NULL"] + [show_deleted ? "WHERE" : "AND"] author LIKE CONCAT('%',:author,'%') + AND title LIKE CONCAT('%',:title,'%') + AND (:category = 'Any' OR category = :category) + [book_id ? "AND id LIKE CONCAT('%', :book_id, '%')" : ""] + AND ckey LIKE CONCAT('%',:ckey,'%') + ORDER BY id DESC + LIMIT :skip, :take + "}, list("author" = author, "title" = title, "book_id" = book_id, "category" = category, "ckey" = ckey, "skip" = BOOKS_PER_PAGE * search_page, "take" = BOOKS_PER_PAGE)) + + var/query_succeeded = query_library_list_books.Execute() + sending_request = FALSE + page_content.Cut() + if(!query_succeeded) + qdel(query_library_list_books) + return + while(query_library_list_books.NextRow()) + page_content += list(list( + "id" = query_library_list_books.item[1], + "author" = html_decode(query_library_list_books.item[2]), + "title" = html_decode(query_library_list_books.item[3]), + "category" = query_library_list_books.item[4], + "author_ckey" = query_library_list_books.item[5], + "deleted" = query_library_list_books.item[6], + )) + qdel(query_library_list_books) + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/update_page_count() + var/bookcount = 0 + var/datum/db_query/query_library_count_books = SSdbcore.NewQuery({" + SELECT COUNT(id) FROM [format_table_name("library")] + [show_deleted ? "" : "WHERE deleted IS NULL"] + [show_deleted ? "WHERE" : "AND"] author LIKE CONCAT('%',:author,'%') + AND title LIKE CONCAT('%',:title,'%') + AND (:category = 'Any' OR category = :category) + [book_id ? "AND id LIKE CONCAT('%', :book_id, '%')" : ""] + AND ckey LIKE CONCAT('%',:ckey,'%') + "}, list("author" = author, "title" = title, "book_id" = book_id, "category" = category, "ckey" = ckey)) + + if(!query_library_count_books.warn_execute()) + qdel(query_library_count_books) + return + if(query_library_count_books.NextRow()) + bookcount = text2num(query_library_count_books.item[1]) + qdel(query_library_count_books) + + page_count = round(max(bookcount - 1, 0) / BOOKS_PER_PAGE) //This is just floor() + search_page = clamp(search_page, 0, page_count) + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/ui_status(mob/user) + if(!check_rights_for(user.client, R_BAN)) + return UI_CLOSE + if(!SSdbcore.Connect()) + can_connect = FALSE + return UI_CLOSE + return UI_INTERACTIVE + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/ui_act(action, params, datum/tgui/ui) + . = ..() + if(.) + // We'll always trigger a search attempt if the parent does something, this ensures the ui is v fast to update + INVOKE_ASYNC(src, PROC_REF(update_db_info)) + return + switch(action) + if("set_search_ckey") + ckey = params["ckey"] + INVOKE_ASYNC(src, PROC_REF(update_db_info)) + return TRUE + if("refresh") + last_search_hash = "" + INVOKE_ASYNC(src, PROC_REF(update_db_info)) + return TRUE + if("hide_book") + var/reason = params["delete_reason"] + var/id = params["book_id"] + var/client/actor = ui.user?.client + if(!actor) + return + INVOKE_ASYNC(src, PROC_REF(hide_book), id, reason, actor) + return TRUE + if("unhide_book") + var/reason = params["free_reason"] + var/id = params["book_id"] + var/client/actor = ui.user?.client + if(!actor) + return + INVOKE_ASYNC(src, PROC_REF(unhide_book), id, reason, actor) + return TRUE + if("get_history") + var/id = params["book_id"] + book_history["[id]"] = get_book_history(id) + return TRUE + if("view_book") + var/id = params["book_id"] + view_book(id, ui.user) + return TRUE + if("toggle_raw") + view_raw = !view_raw + return TRUE + if("toggle_deleted") + show_deleted = !show_deleted + INVOKE_ASYNC(src, PROC_REF(update_db_info)) + return TRUE + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/ui_data(mob/user) + . = ..() + .["view_raw"] = view_raw + .["show_deleted"] = show_deleted + var/list/histories = list() + for(var/id as anything in book_history) + var/list/insert = list() + for(var/datum/book_history_entry/entry in book_history[id]) + insert += list(entry.serialize()) + histories[id] = insert + .["history"] = histories + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/proc/view_book(id, mob/show_to) + if (!SSdbcore.Connect()) + can_connect = FALSE + message_admins("Failed to establish database connection.") + return + + var/datum/db_query/query_library_view = SSdbcore.NewQuery( + "SELECT * FROM [format_table_name("library")] WHERE id=:id", + list("id" = id) + ) + if(!query_library_view.Execute()) + qdel(query_library_view) + return + + while(query_library_view.NextRow()) + var/datum/admin_book_viewer/viewer = new() + viewer.set_owner(src) + viewer.id = query_library_view.item[1] + viewer.author = query_library_view.item[2] + viewer.title = query_library_view.item[3] + viewer.content = query_library_view.item[4] + viewer.category = query_library_view.item[5] + viewer.author_ckey = query_library_view.item[6] + viewer.creation_time = query_library_view.item[7] + viewer.deleted = query_library_view.item[8] + viewer.creation_round = query_library_view.item[9] + viewer.history = get_book_history(id) + viewer.ui_interact(show_to) + break + qdel(query_library_view) + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/proc/get_book_history(id) + var/datum/db_query/query_book_history = SSdbcore.NewQuery({" + SELECT id, book, reason, ckey, datetime, action, INET_NTOA(ip_addr) + FROM [format_table_name("library_action")] WHERE book=:id + "}, + list("id" = id) + ) + if(!query_book_history.Execute()) + qdel(query_book_history) + return list() + + var/list/full_history = list() + while(query_book_history.NextRow()) + var/datum/book_history_entry/history = new() + history.id = query_book_history.item[1] + history.book = query_book_history.item[2] + history.reason = query_book_history.item[3] + history.ckey = query_book_history.item[4] + history.datetime = query_book_history.item[5] + history.action = query_book_history.item[6] + history.ip_addr = query_book_history.item[7] + full_history += history + qdel(query_book_history) + return full_history + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/proc/hide_book(id, reason, client/admin) + if(!SSdbcore.Connect()) + can_connect = FALSE + to_chat(admin, span_danger("Failed to establish database connection.")) + return + if(!check_rights_for(admin, R_BAN)) + log_admin_private("[admin.ckey] tried to hide a book without the required perms") + message_admins("[admin.ckey] tried to hide a book without the required perms") + return + + var/datum/db_query/query_hide_book = SSdbcore.NewQuery({" + UPDATE [format_table_name("library")] + SET deleted = 1 + WHERE id = :id + "}, list("id" = id)) + if(!query_hide_book.warn_execute()) + qdel(query_hide_book) + return + qdel(query_hide_book) + + + var/datum/db_query/query_update_log = SSdbcore.NewQuery({" + INSERT INTO [format_table_name("library_action")] (book, reason, ckey, datetime, action, ip_addr) + VALUES (:book, :reason, :ckey, Now(), :action, INET_ATON(:ip_addr)) + "}, list("book" = id, "reason" = reason, "ckey" = admin.ckey, "action" = BOOK_ADMIN_DELETE, "ip_addr" = admin.address)) + if(!query_update_log.warn_execute()) + qdel(query_update_log) + return + qdel(query_update_log) + + var/log_reason = "([admin.ckey]) hid book #[id][reason ? ": \"[reason]\"" : ""]" + log_admin_private(log_reason) + library_updated() + update_db_info() + +/obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/proc/unhide_book(id, reason, client/admin) + if(!SSdbcore.Connect()) + can_connect = FALSE + to_chat(admin, span_danger("Failed to establish database connection.")) + return + if(!check_rights_for(admin, R_BAN)) + log_admin_private("[admin.ckey] tried to unhide a book without the required perms") + message_admins("[admin.ckey] tried to unhide a book without the required perms") + return + + var/datum/db_query/query_unhide_book = SSdbcore.NewQuery({" + UPDATE [format_table_name("library")] + SET deleted = NULL + WHERE id = :id + "}, list("id" = id)) + + if(!query_unhide_book.warn_execute()) + qdel(query_unhide_book) + return + qdel(query_unhide_book) + + var/datum/db_query/query_update_log = SSdbcore.NewQuery({" + INSERT INTO [format_table_name("library_action")] (book, reason, ckey, datetime, action, ip_addr) + VALUES (:book, :reason, :ckey, Now(), :action, INET_ATON(:ip_addr)) + "}, list("book" = id, "reason" = reason, "ckey" = admin.ckey, "action" = BOOK_ADMIN_RESTORE, "ip_addr" = admin.address)) + if(!query_update_log.warn_execute()) + qdel(query_update_log) + return + qdel(query_update_log) + + log_admin_private("([admin.ckey]) unhid book #[id]") + library_updated() + update_db_info() + +/// This mostly exists to document the form of the library_action table, since it doesn't do that good a job on its own +/datum/book_history_entry + /// The id of this logged action + var/id + /// The book id this log applies to + var/book + /// The reason this action was enacted + var/reason + /// The admin who performed the action + var/ckey + /// The time of the action being performed + var/datetime + /// The action that occured (BOOK_ADMIN_DELETE, BOOK_ADMIN_RESTORE, and legacy BOOK_ADMIN_REPORT) + var/action + /// The ip address of the admin who performed the action + var/ip_addr + +/datum/book_history_entry/proc/serialize() + var/list/data = list() + data["id"] = id + data["book"] = book + data["reason"] = reason + data["ckey"] = ckey + data["datetime"] = datetime + data["action"] = action + data["address"] = ip_addr + return data + +/// Weaps around a book's sql data, feeds it into a ui that allows us to at base view the contents of the book +/datum/admin_book_viewer + /// Weakref to the /obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker that spawned us + var/datum/weakref/owner_ref + /// If we're displaying raw data or rendered markdown + var/view_raw = FALSE + /// The book id. Incremental, goes up over time + var/id + /// The display name for the book, taken from the player's character + var/author + /// Title of the book + var/title + /// The full text of the book, stored raw + var/content + /// Category the book falls into, see SSlibrary.search_categories + var/category + /// The ckey of the user who triggered the upload request + var/author_ckey + /// The time of day at which the book was uploaded + var/creation_time + /// Boolean, flips to true to "hide" a book from public viewing. Defaults to null + var/deleted + /// The round id the book was uploaded in + var/creation_round + /// Represents the full admin record of this book, as of the view request. Datumized to make it easier to deal with. + var/list/datum/book_history_entry/history + +/datum/admin_book_viewer/proc/set_owner(obj/machinery/computer/libraryconsole/admin_only_do_not_map_in_you_fucker/owner) + owner_ref = WEAKREF(owner) + view_raw = owner.view_raw + +/datum/admin_book_viewer/ui_interact(mob/user, datum/tgui/ui) + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AdminBookViewer") + ui.set_autoupdate(FALSE) // Nothing is changing here brother + ui.open() + +/datum/admin_book_viewer/ui_status(mob/user) + if(!check_rights_for(user.client, R_BAN)) + return UI_CLOSE + return UI_INTERACTIVE + +/datum/admin_book_viewer/ui_data(mob/user) + var/list/data = list() + data["view_raw"] = view_raw + data["id"] = id + data["author"] = author + data["title"] = title + data["content"] = content + data["category"] = category + data["author_ckey"] = author_ckey + data["creation_time"] = creation_time + data["deleted"] = deleted + data["creation_round"] = creation_round + data["history"] = list() + for(var/datum/book_history_entry/entry as anything in history) + data["history"] += list(entry.serialize()) + + return data + +#undef BOOK_ADMIN_DELETE +#undef BOOK_ADMIN_RESTORE +#undef BOOK_ADMIN_REPORT diff --git a/code/modules/library/lib_machines.dm b/code/modules/library/lib_machines.dm index d66ed54d60b..0f6b94cd003 100644 --- a/code/modules/library/lib_machines.dm +++ b/code/modules/library/lib_machines.dm @@ -9,15 +9,12 @@ * Book Binder */ -#define DEFAULT_UPLOAD_CATAGORY "Fiction" -#define DEFAULT_SEARCH_CATAGORY "Any" +GLOBAL_VAR_INIT(library_table_modified, 0) + +/// Increments every time WE update the library db table, causes all existing consoles to repull when they next check +/proc/library_updated() + GLOB.library_table_modified = (GLOB.library_table_modified + 1) % (SHORT_REAL_LIMIT - 1) -///How many books should we load per page? -#define BOOKS_PER_PAGE 18 -///How many checkout records should we load per page? -#define CHECKOUTS_PER_PAGE 17 -///How many inventory items should we load per page? -#define INVENTORY_PER_PAGE 19 /* * Library Public Computer */ @@ -29,6 +26,8 @@ circuit = /obj/item/circuitboard/computer/libraryconsole desc = "Checked out books MUST be returned on time." anchored_tabletop_offset = 8 + ///The current book id we're searching for + var/book_id = null ///The current title we're searching for var/title = "" ///The category we're searching for @@ -73,6 +72,7 @@ data["category"] = category data["author"] = author data["title"] = title + data["book_id"] = book_id data["page_count"] = page_count + 1 //Increase these by one so it looks like we're not indexing at 0 data["our_page"] = search_page + 1 data["pages"] = page_content @@ -85,6 +85,12 @@ if(.) return switch(action) + if("set_search_id") + var/newid = text2num(params["id"]) + if(newid != book_id) + params_changed = TRUE + book_id = newid + return TRUE if("set_search_title") var/newtitle = params["title"] if(newtitle != title) @@ -184,7 +190,7 @@ return TRUE /obj/machinery/computer/libraryconsole/proc/hash_search_info() - return "[title]-[author]-[category]-[search_page]-[page_count]" + return "[GLOB.library_table_modified]-[book_id]-[title]-[author]-[category]-[search_page]-[page_count]" /obj/machinery/computer/libraryconsole/proc/update_page_contents() if(sending_request) //Final defense against nerds spamming db requests @@ -198,9 +204,10 @@ AND author LIKE CONCAT('%',:author,'%') AND title LIKE CONCAT('%',:title,'%') AND (:category = 'Any' OR category = :category) + [book_id ? "AND id LIKE CONCAT('%', :book_id, '%')" : ""] ORDER BY id DESC LIMIT :skip, :take - "}, list("author" = author, "title" = title, "category" = category, "skip" = BOOKS_PER_PAGE * search_page, "take" = BOOKS_PER_PAGE)) + "}, list("author" = author, "title" = title, "book_id" = book_id, "category" = category, "skip" = BOOKS_PER_PAGE * search_page, "take" = BOOKS_PER_PAGE)) var/query_succeeded = query_library_list_books.Execute() sending_request = FALSE @@ -225,7 +232,8 @@ AND author LIKE CONCAT('%',:author,'%') AND title LIKE CONCAT('%',:title,'%') AND (:category = 'Any' OR category = :category) - "}, list("author" = author, "title" = title, "category" = category)) + [book_id ? "AND id LIKE CONCAT('%', :book_id, '%')" : ""] + "}, list("author" = author, "title" = title, "book_id" = book_id, "category" = category)) if(!query_library_count_books.warn_execute()) qdel(query_library_count_books) @@ -299,8 +307,6 @@ var/inventory_page = 0 ///Should we load our inventory from the bookselves in our area? var/dynamic_inv_load = FALSE - ///Toggled if some bit of code wants to override hashing and allow for page updates - var/ignore_hash = FALSE ///Book scanner that will be used when uploading books to the Archive var/datum/weakref/scanner ///Our cooldown on using the printer @@ -553,14 +559,6 @@ return obj_flags |= EMAGGED -/obj/machinery/computer/libraryconsole/bookmanagement/has_anything_changed() - if(..()) - return TRUE - if(!ignore_hash) - return FALSE - ignore_hash = FALSE - return TRUE - /obj/machinery/computer/libraryconsole/bookmanagement/proc/set_screen_state(new_state) screen_state = clamp(new_state, MIN_LIBRARY, MAX_LIBRARY) @@ -617,8 +615,8 @@ return usr.log_message(msg, LOG_GAME) qdel(query_library_upload) + library_updated() say("Upload Complete. Uploaded title will be available for printing in a moment") - ignore_hash = TRUE update_db_info() /// Call this proc to attempt a print. It will return false if the print failed, true otherwise, longside some ux @@ -828,11 +826,6 @@ qdel(draw_from) -#undef BOOKS_PER_PAGE -#undef CHECKOUTS_PER_PAGE -#undef DEFAULT_SEARCH_CATAGORY -#undef DEFAULT_UPLOAD_CATAGORY -#undef INVENTORY_PER_PAGE #undef LIBRARY_ARCHIVE #undef LIBRARY_CHECKOUT #undef LIBRARY_INVENTORY diff --git a/tgstation.dme b/tgstation.dme index 0f1f20f3146..0070c6de932 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -125,6 +125,7 @@ #include "code\__DEFINES\language.dm" #include "code\__DEFINES\layers.dm" #include "code\__DEFINES\lazy_templates.dm" +#include "code\__DEFINES\library.dm" #include "code\__DEFINES\lighting.dm" #include "code\__DEFINES\lights.dm" #include "code\__DEFINES\living.dm" @@ -3814,6 +3815,7 @@ #include "code\modules\language\uncommon.dm" #include "code\modules\language\voltaic.dm" #include "code\modules\language\xenocommon.dm" +#include "code\modules\library\admin_only.dm" #include "code\modules\library\barcode_scanner.dm" #include "code\modules\library\bibles.dm" #include "code\modules\library\book.dm" diff --git a/tgui/packages/tgui/interfaces/AdminBookViewer.tsx b/tgui/packages/tgui/interfaces/AdminBookViewer.tsx new file mode 100644 index 00000000000..5996b3937d7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AdminBookViewer.tsx @@ -0,0 +1,25 @@ +import { useBackend } from '../backend'; +import { Window } from '../layouts'; +import { MarkdownRenderer } from './MarkdownViewer'; + +type ViewerData = { + title: string; + content: string; + author: string; + view_raw: boolean; +}; + +export const AdminBookViewer = (_: any, context: any) => { + const { data } = useBackend(context); + return ( + + + {data.view_raw ? ( + data.content + ) : ( + + )} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/LibraryAdmin.tsx b/tgui/packages/tgui/interfaces/LibraryAdmin.tsx new file mode 100644 index 00000000000..d7720db19db --- /dev/null +++ b/tgui/packages/tgui/interfaces/LibraryAdmin.tsx @@ -0,0 +1,487 @@ +import { map, sortBy } from 'common/collections'; +import { flow } from 'common/fp'; +import { capitalize } from 'common/string'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Dropdown, Input, NoticeBox, Section, Stack, Table, TextArea } from '../components'; +import { Window } from '../layouts'; +import { PageSelect } from './LibraryConsole'; + +export const LibraryAdmin = (props, context) => { + const [modifyMethod, setModifyMethod] = useLocalState( + context, + 'ModifyMethod', + null + ); + return ( + + {modifyMethod ? : } + + ); +}; + +type ListingData = { + can_connect: boolean; + can_db_request: boolean; + our_page: number; + page_count: number; +}; + +const BookListing = (props, context) => { + const { act, data } = useBackend(context); + const { can_connect, can_db_request, our_page, page_count } = data; + if (!can_connect) { + return ( + + Unable to retrieve book listings. Please contact your system + administrator for assistance. + + ); + } + return ( + + + + + + + + + + + act('switch_page', { + page: value, + }) + } + /> + + + ); +}; + +type Book = { + author: string; + category: string; + title: string; + id: number; + deleted: boolean; +}; + +type DisplayBook = Book & { + key: number; +}; + +type DisplayData = { + can_db_request: boolean; + search_categories: string[]; + book_id: number; + title: string; + category: string; + author: string; + author_ckey: string; + params_changed: boolean; + view_raw: boolean; + show_deleted: boolean; + history: HistoryArray; + pages: Book[]; +}; + +const SearchAndDisplay = (props, context) => { + const { act, data } = useBackend(context); + const [modifyMethod, setModifyMethod] = useLocalState( + context, + 'ModifyMethod', + '' + ); + const [modifyTarget, setModifyTarget] = useLocalState( + context, + 'ModifyTarget', + 0 + ); + const { + can_db_request, + search_categories = [], + book_id, + title, + category, + author, + author_ckey, + pages, + params_changed, + view_raw, + show_deleted, + } = data; + const books = flow([ + map((book, i) => ({ + ...book, + // Generate a unique id + key: i, + })), + sortBy((book) => book.key), + ])(pages); + return ( +
+ + + + + + act('set_search_id', { + id: value, + }) + } + /> + + + + act('set_search_category', { + category: value, + }) + } + /> + + + + act('set_search_title', { + title: value, + }) + } + /> + + + + act('set_search_author', { + author: value, + }) + } + /> + + + + act('set_search_ckey', { + ckey: value, + }) + } + /> + + + + + + + + + + +
+ ); +}; + +const ModifyTypes = { + Delete: 'delete', + Restore: 'restore', +}; + +type HistoryEntry = { + // The id of this logged action + id: number; + // The book id this log applies to + book: number; + // The reason this action was enacted + reason: string; + // The admin who performed the action + ckey: string; + // The time of the action being performed + datetime: string; + // The action that ocurred + action: string; + // The ip address of the admin who performed the action + ip_addr: string; +}; + +type HistoryArray = { + [key: string]: HistoryEntry[]; +}; + +type ModalData = { + can_db_request: boolean; + view_raw: boolean; + history: HistoryArray; +}; + +const ModifyPage = (props, context) => { + const { act, data } = useBackend(context); + + const { can_db_request, view_raw, history } = data; + const [modifyMethod, setModifyMethod] = useLocalState( + context, + 'ModifyMethod', + '' + ); + const [modifyTarget, setModifyTarget] = useLocalState( + context, + 'ModifyTarget', + 0 + ); + const [reason, setReason] = useLocalState(context, 'Reason', 'null'); + + const entries = history[modifyTarget.toString()] + ? history[modifyTarget.toString()].sort((a, b) => b.id - a.id) + : []; + + return ( + + + Heads Up! We do not allow you to fully delete books in game +
+ What you're doing here is a "don't show this to + anyone" button +
+ If you for whatever reason need to fully wipe a book, please speak to + your database administrator +
+ + + Why do you want to {modifyMethod} this book? + + + + + +