mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-14 19:03:21 +00:00
For the longest time, the only way admins could moderate the library was by using statbus's external tool. But a few months back statbus went down, and ever since then they've been sitting lost. Shit sucks. The whole external thing has been bugging me for a while, so let's fix all that yeah? This pr adds a new verb to the admin tab that allows admins to ban/restore books from the library. It includes expanded (ckey) search, faster response times, in tool book viewing with and without markdown rendering, and viewing of deleted books. This is accomplished with a special subtype of library consoles, stored on the admin datum. It shouldn't let you do anything without +BAN, rip my live debugging or whatever. I've also hooked into (and fixed) Ned's existing library actions log, and added viewing support to the ban/restore pages. This logs banning admin, ban time, ban reason, etc. As a part of this, I've fixed/expanded on the existing UIs. I've added ID search to all existing consoles, and fixed an existing bug with the visitor console not supporting category search (shows how many people actually use the thing) Changes to the library_action table were pretty minor. The ckey column was too small, so longer keys just caused it to fail on ban. Bad. That and the ip address column was signed, which wasted space and was non standard with other tables.
368 lines
13 KiB
Plaintext
368 lines
13 KiB
Plaintext
#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
|