diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.txt index 6209e6872f4..2c538a1d8f3 100644 --- a/SQL/database_changelog.txt +++ b/SQL/database_changelog.txt @@ -1,13 +1,21 @@ Any time you make a change to the schema files, remember to increment the database schema version. Generally increment the minor number, major should be reserved for significant changes to the schema. Both values go up to 255. -The latest database version is 5.17; The query to update the schema revision table is: +The latest database version is 5.19; The query to update the schema revision table is: -INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 18); +INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 19); or -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 18); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 19); In any query remember to add a prefix to the table names if you use one. +----------------------------------------------------- +Version 5.19, 10 November 2021, by WalterMeldron +Adds an urgent column to tickets for ahelps marked as urgent. + +``` +ALTER TABLE `ticket` ADD COLUMN `urgent` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `sender`; +``` + ----------------------------------------------------- Version 5.18, 1 November 2021, by Mothblocks Added `known_alts` table for tracking who not to create suspicious logins for. diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index ac913be0149..2a5bb7bfde5 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -588,6 +588,7 @@ CREATE TABLE `ticket` ( `round_id` int(11) unsigned NULL, `ticket` smallint(11) unsigned NOT NULL, `action` varchar(20) NOT NULL DEFAULT 'Message', + `urgent` TINYINT(1) unsigned NOT NULL DEFAULT '0', `message` text NOT NULL, `timestamp` datetime NOT NULL, `recipient` varchar(32) DEFAULT NULL, diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql index bcb26de7d25..c599e3466b8 100644 --- a/SQL/tgstation_schema_prefixed.sql +++ b/SQL/tgstation_schema_prefixed.sql @@ -588,6 +588,7 @@ CREATE TABLE `SS13_ticket` ( `round_id` int(11) unsigned NULL, `ticket` smallint(11) unsigned NOT NULL, `action` varchar(20) NOT NULL DEFAULT 'Message', + `urgent` TINYINT(1) unsigned NOT NULL DEFAULT '0', `message` text NOT NULL, `timestamp` datetime NOT NULL, `recipient` varchar(32) DEFAULT NULL, diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 32a3be09510..94fd44f7a3c 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -20,7 +20,7 @@ * * make sure you add an update to the schema_version stable in the db changelog */ -#define DB_MINOR_VERSION 18 +#define DB_MINOR_VERSION 19 //! ## Timing subsystem diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm index 230dd03361e..fe291f690ef 100644 --- a/code/__HELPERS/chat.dm +++ b/code/__HELPERS/chat.dm @@ -40,21 +40,22 @@ In TGS3 it will always be sent to all connected designated game chats. * * message - The message to send. * channel_tag - Required. If "", the message with be sent to all connected (Game-type for TGS3) channels. Otherwise, it will be sent to TGS4 channels with that tag (Delimited by ','s). + * admin_only - Determines if this communication can only be sent to admin only channels. */ -/proc/send2chat(message, channel_tag) +/proc/send2chat(message, channel_tag, admin_only = FALSE) if(channel_tag == null || !world.TgsAvailable()) return var/datum/tgs_version/version = world.TgsVersion() if(channel_tag == "" || version.suite == 3) - world.TgsTargetedChatBroadcast(message, FALSE) + world.TgsTargetedChatBroadcast(message, admin_only) return var/list/channels_to_use = list() for(var/I in world.TgsChatChannelInfo()) var/datum/tgs_chat_channel/channel = I var/list/applicable_tags = splittext(channel.custom_tag, ",") - if(channel_tag in applicable_tags) + if((!admin_only || channel.is_admin_channel) && (channel_tag in applicable_tags)) channels_to_use += channel if(channels_to_use.len) diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index b142ce6217e..a9d4c2028b9 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -543,3 +543,18 @@ min_val = 0 /datum/config_entry/str_list/motd + +/datum/config_entry/number/urgent_ahelp_cooldown + default = 300 + +/datum/config_entry/string/urgent_ahelp_message + default = "This ahelp is urgent!" + +/datum/config_entry/string/urgent_ahelp_user_prompt + default = "There are no admins currently on. Do not press the button below if your ahelp is a joke, a request or a question. Use it only for cases of obvious grief." + +/datum/config_entry/string/adminhelp_webhook_url + +/datum/config_entry/string/adminhelp_webhook_pfp + +/datum/config_entry/string/adminhelp_webhook_name diff --git a/code/controllers/subsystem/blackbox.dm b/code/controllers/subsystem/blackbox.dm index abc5a6a02f1..0b195613417 100644 --- a/code/controllers/subsystem/blackbox.dm +++ b/code/controllers/subsystem/blackbox.dm @@ -288,14 +288,25 @@ Versioning key = new_key key_type = new_key_type -/datum/controller/subsystem/blackbox/proc/LogAhelp(ticket, action, message, recipient, sender) +/datum/controller/subsystem/blackbox/proc/LogAhelp(ticket, action, message, recipient, sender, urgent = FALSE) if(!SSdbcore.Connect()) return var/datum/db_query/query_log_ahelp = SSdbcore.NewQuery({" - INSERT INTO [format_table_name("ticket")] (ticket, action, message, recipient, sender, server_ip, server_port, round_id, timestamp) - VALUES (:ticket, :action, :message, :recipient, :sender, INET_ATON(:server_ip), :server_port, :round_id, :time) - "}, list("ticket" = ticket, "action" = action, "message" = message, "recipient" = recipient, "sender" = sender, "server_ip" = world.internet_address || "0", "server_port" = world.port, "round_id" = GLOB.round_id, "time" = SQLtime())) + INSERT INTO [format_table_name("ticket")] (ticket, action, message, recipient, sender, server_ip, server_port, round_id, timestamp, urgent) + VALUES (:ticket, :action, :message, :recipient, :sender, INET_ATON(:server_ip), :server_port, :round_id, :time, :urgent) + "}, list( + "ticket" = ticket, + "action" = action, + "message" = message, + "recipient" = recipient, + "sender" = sender, + "server_ip" = world.internet_address || "0", + "server_port" = world.port, + "round_id" = GLOB.round_id, + "time" = SQLtime(), + "urgent" = urgent, + )) query_log_ahelp.Execute() qdel(query_log_ahelp) diff --git a/code/datums/keybinding/client.dm b/code/datums/keybinding/client.dm index e4b940c7f72..5b23c823ddb 100644 --- a/code/datums/keybinding/client.dm +++ b/code/datums/keybinding/client.dm @@ -14,7 +14,7 @@ . = ..() if(.) return - user.get_adminhelp() + user.adminhelp() return TRUE diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index 8715d2a6690..c7f45de4c6d 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -279,7 +279,7 @@ break_counter = 0 var/list/other_job_lists = list( - "Abstract" = list("Appearance", "Emote", "Deadchat", "OOC"), + "Abstract" = list("Appearance", "Emote", "Deadchat", "OOC", "Urgent Adminhelp"), ) for(var/department in other_job_lists) output += "
" diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 9f54602a142..22a583bca4a 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -192,7 +192,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) * * msg_raw - The first message of this admin_help: used for the initial title of the ticket * * is_bwoink - Boolean operator, TRUE if this ticket was started by an admin PM */ -/datum/admin_help/New(msg_raw, client/C, is_bwoink) +/datum/admin_help/New(msg_raw, client/C, is_bwoink, urgent = FALSE) //clean the input msg var/msg = sanitize(copytext_char(msg_raw, 1, MAX_MESSAGE_LEN)) if(!msg || !C || !C.mob) @@ -222,16 +222,46 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) AddInteraction("[key_name_admin(usr)] PM'd [LinkedReplyName()]") message_admins("Ticket [TicketHref("#[id]")] created") else - MessageNoRecipient(msg_raw) - - //send it to TGS if nobody is on and tell us how many were on - var/admin_number_present = send2tgs_adminless_only(initiator_ckey, "Ticket #[id]: [msg]") - log_admin_private("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.") - if(admin_number_present <= 0) - to_chat(C, span_notice("No active admins are online, your adminhelp was sent through TGS to admins who are available. This may use IRC or Discord."), confidential = TRUE) - heard_by_no_admins = TRUE + MessageNoRecipient(msg_raw, urgent) + send_message_to_tgs(msg, urgent) GLOB.ahelp_tickets.active_tickets += src +/datum/admin_help/proc/send_message_to_tgs(message, urgent = FALSE) + var/message_to_send = message + + if(urgent) + var/extra_message_to_send = "[message] - Requested an admin" + var/extra_message = CONFIG_GET(string/urgent_ahelp_message) + if(extra_message) + extra_message_to_send += " ([extra_message])" + to_chat(initiator, span_boldwarning("Notified admins to prioritize your ticket")) + send2adminchat_webhook("RELAY: [initiator_ckey] | Ticket #[id]: [extra_message_to_send]") + //send it to TGS if nobody is on and tell us how many were on + var/admin_number_present = send2tgs_adminless_only(initiator_ckey, "Ticket #[id]: [message_to_send]") + log_admin_private("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.") + if(admin_number_present <= 0) + to_chat(initiator, span_notice("No active admins are online, your adminhelp was sent to admins who are available through IRC or Discord."), confidential = TRUE) + heard_by_no_admins = TRUE + +/proc/send2adminchat_webhook(message) + if(!CONFIG_GET(string/adminhelp_webhook_url)) + return + var/message_content = replacetext(replacetext(message, "\proper", ""), "\improper", "") + message_content = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ") + var/list/webhook_info = list() + webhook_info["content"] = message_content + if(CONFIG_GET(string/adminhelp_webhook_name)) + webhook_info["username"] = CONFIG_GET(string/adminhelp_webhook_name) + if(CONFIG_GET(string/adminhelp_webhook_pfp)) + webhook_info["avatar_url"] = CONFIG_GET(string/adminhelp_webhook_pfp) + // Uncomment when servers are moved to TGS4 + // send2chat("[initiator_ckey] | [message_content]", "ahelp", TRUE) + var/list/headers = list() + headers["Content-Type"] = "application/json" + var/datum/http_request/request = new() + request.prepare(RUSTG_HTTP_METHOD_POST, CONFIG_GET(string/adminhelp_webhook_url), json_encode(webhook_info), headers, "tmp/response.json") + request.begin_async() + /datum/admin_help/Destroy() RemoveActive() GLOB.ahelp_tickets.closed_tickets -= src @@ -282,7 +312,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) //message from the initiator without a target, all admins will see this //won't bug irc/discord -/datum/admin_help/proc/MessageNoRecipient(msg) +/datum/admin_help/proc/MessageNoRecipient(msg, urgent = FALSE) msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN)) var/ref_src = "[REF(src)]" //Message to be sent to all admins @@ -306,7 +336,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) type = MESSAGE_TYPE_ADMINPM, html = span_adminnotice("PM to-Admins: [msg]"), confidential = TRUE) - SSblackbox.LogAhelp(id, "Ticket Opened", msg, null, initiator.ckey) + SSblackbox.LogAhelp(id, "Ticket Opened", msg, null, initiator.ckey, urgent = urgent) //Reopen a closed ticket /datum/admin_help/proc/Reopen() @@ -535,38 +565,100 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) deltimer(adminhelptimerid) adminhelptimerid = 0 -// Used for methods where input via arg doesn't work -/client/proc/get_adminhelp() - var/msg = input(src, "Please describe your problem concisely and an admin will help as soon as they're able.", "Adminhelp contents") as message|null - adminhelp(msg) +GLOBAL_DATUM_INIT(admin_help_ui_handler, /datum/admin_help_ui_handler, new) -/client/verb/adminhelp(msg as message) - set category = "Admin" - set name = "Adminhelp" +/datum/admin_help_ui_handler + var/list/ahelp_cooldowns = list() +/datum/admin_help_ui_handler/ui_state(mob/user) + return GLOB.always_state + +/datum/admin_help_ui_handler/ui_data(mob/user) + . = list() + var/list/admins = get_admin_counts(R_BAN) + .["adminCount"] = length(admins["present"]) + +/datum/admin_help_ui_handler/ui_static_data(mob/user) + . = list() + .["bannedFromUrgentAhelp"] = is_banned_from(user.ckey, "Urgent Adminhelp") + .["urgentAhelpPromptMessage"] = CONFIG_GET(string/urgent_ahelp_user_prompt) + var/webhook_url = CONFIG_GET(string/adminhelp_webhook_url) + if(webhook_url) + .["urgentAhelpEnabled"] = TRUE + +/datum/admin_help_ui_handler/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Adminhelp") + ui.open() + ui.set_autoupdate(FALSE) + +/datum/admin_help_ui_handler/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + var/client/user_client = usr.client + var/message = sanitize_text(trim(params["message"])) + var/urgent = !!params["urgent"] + var/list/admins = get_admin_counts(R_BAN) + if(length(admins["present"]) != 0 || is_banned_from(user_client.ckey, "Urgent Adminhelp")) + urgent = FALSE + + if(user_client.adminhelptimerid) + return + + perform_adminhelp(user_client, message, urgent) + ui.close() + +/datum/admin_help_ui_handler/proc/perform_adminhelp(client/user_client, message, urgent) if(GLOB.say_disabled) //This is here to try to identify lag problems to_chat(usr, span_danger("Speech is currently admin-disabled."), confidential = TRUE) return + if(!message) + return + //handle muting and automuting - if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, span_danger("Error: Admin-PM: You cannot send adminhelps (Muted)."), confidential = TRUE) + if(user_client.prefs.muted & MUTE_ADMINHELP) + to_chat(user_client, span_danger("Error: Admin-PM: You cannot send adminhelps (Muted)."), confidential = TRUE) return - if(handle_spam_prevention(msg,MUTE_ADMINHELP)) - return - - msg = trim(msg) - - if(!msg) + if(user_client.handle_spam_prevention(message, MUTE_ADMINHELP)) return SSblackbox.record_feedback("tally", "admin_verb", 1, "Adminhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - if(current_ticket) - current_ticket.MessageNoRecipient(msg) - current_ticket.TimeoutVerb() + + if(urgent) + if(!COOLDOWN_FINISHED(src, ahelp_cooldowns?[user_client.ckey])) + urgent = FALSE // Prevent abuse + else + COOLDOWN_START(src, ahelp_cooldowns[user_client.ckey], CONFIG_GET(number/urgent_ahelp_cooldown) * (1 SECONDS)) + + if(user_client.current_ticket) + user_client.current_ticket.TimeoutVerb() + if(urgent) + var/sanitized_message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN)) + user_client.current_ticket.send_message_to_tgs(sanitized_message, urgent = TRUE) + user_client.current_ticket.MessageNoRecipient(message, urgent) return - new /datum/admin_help(msg, src, FALSE) + new /datum/admin_help(message, user_client, FALSE, urgent) + +/client/verb/no_tgui_adminhelp(message as message) + set name = "NoTguiAdminhelp" + set hidden = TRUE + + if(adminhelptimerid) + return + + message = trim(message) + + GLOB.admin_help_ui_handler.perform_adminhelp(src, message, FALSE) + +/client/verb/adminhelp() + set category = "Admin" + set name = "Adminhelp" + GLOB.admin_help_ui_handler.ui_interact(mob) + to_chat(src, span_boldnotice("Adminhelp failing to open or work? Click here")) // diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 189dd221af4..00253cdedaa 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -32,6 +32,9 @@ var/externalreplyamount = 0 ///Tracks say() usage for ic/dchat while slowmode is enabled COOLDOWN_DECLARE(say_slowmode) + /// The last urgent ahelp that this player sent + COOLDOWN_DECLARE(urgent_ahelp_cooldown) + ///////// //OTHER// ///////// diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 01fff405322..50e0cdc72d0 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -100,6 +100,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(href_list["priv_msg"]) cmd_admin_pm(href_list["priv_msg"],null) return + // TGUIless adminhelp + if(href_list["tguiless_adminhelp"]) + no_tgui_adminhelp(input(src, "Enter your ahelp", "Ahelp") as null|message) + return switch(href_list["_src_"]) if("holder") diff --git a/config/config.txt b/config/config.txt index ab6a4750f8e..4cf96fbd34c 100644 --- a/config/config.txt +++ b/config/config.txt @@ -559,6 +559,25 @@ DEFAULT_VIEW_SQUARE 15x15 ## Once a typepath causes overrun from hard deletes this many times, stop hard deleting it on garbage collection failures. (set to 0 to disable) #HARD_DELETES_OVERRUN_LIMIT 0 +## The URL to the webhook for adminhelps to relay the urgent ahelp message to +## If not set, will disable urgent ahelps. +#ADMINHELP_WEBHOOK_URL + +## The urgent ahelp cooldown for a given player if they're alone on a server and need to send an urgent ahelp. +URGENT_AHELP_COOLDOWN 300 + +## The message that is sent to the discord if an urgent ahelp is sent. Useful for sending a role ping. +#URGENT_AHELP_MESSAGE Ban ban ban ban + +## The message that the player receives whenever prompted whether they want to send an urgent ahelp or not. +#URGENT_AHELP_USER_PROMPT This'll ping the admins! + +## The URL for the pfp of the webhook +#ADMINHELP_WEBHOOK_PFP + +## The name of the webhook +#ADMINHELP_WEBHOOK_NAME + ## Sets an MOTD of the server. ## You can use this multiple times, and the MOTDs will be appended in order. ## Based on config directory, so "motd.txt" points to "config/motd.txt" diff --git a/tgui/packages/tgui/components/TextArea.js b/tgui/packages/tgui/components/TextArea.js index 00e1605a3b3..4b6fda547cb 100644 --- a/tgui/packages/tgui/components/TextArea.js +++ b/tgui/packages/tgui/components/TextArea.js @@ -104,6 +104,16 @@ export class TextArea extends Component { if (input) { input.value = toInputValue(nextValue); } + + if (this.props.autoFocus || this.props.autoSelect) { + setTimeout(() => { + input.focus(); + + if (this.props.autoSelect) { + input.select(); + } + }, 1); + } } componentDidUpdate(prevProps, prevState) { diff --git a/tgui/packages/tgui/interfaces/Adminhelp.tsx b/tgui/packages/tgui/interfaces/Adminhelp.tsx new file mode 100644 index 00000000000..02545a49084 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Adminhelp.tsx @@ -0,0 +1,111 @@ +import { BooleanLike } from "common/react"; +import { useBackend, useLocalState } from "../backend"; +import { TextArea, Stack, Button, NoticeBox, Input, Box } from "../components"; +import { Window } from "../layouts"; + +type AdminhelpData = { + adminCount: number, + urgentAhelpEnabled: BooleanLike, + bannedFromUrgentAhelp: BooleanLike, + urgentAhelpPromptMessage: string, +} + +export const Adminhelp = (props, context) => { + const { act, data } = useBackend(context); + const { + adminCount, + urgentAhelpEnabled, + bannedFromUrgentAhelp, + urgentAhelpPromptMessage, + } = data; + const [requestForAdmin, setRequestForAdmin] = useLocalState(context, "request_for_admin", false); + const [currentlyInputting, setCurrentlyInputting] = useLocalState(context, "confirm_request", false); + const [ahelpMessage, setAhelpMessage] = useLocalState(context, "ahelp_message", ""); + + const confirmationText = "alert admins"; + return ( + + + + +