From def879e5b4b9856f5ef8015de5113cd057955fa5 Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Thu, 4 Aug 2022 16:47:52 +0200 Subject: [PATCH] [MIRROR] Refactors admin PMs to be hopefully more resilient [MDB IGNORE] (#15348) * Refactors admin PMs to be hopefully more resilient * autoconflict resolution Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Co-authored-by: Gandalf <9026500+Gandalf2k15@users.noreply.github.com> --- code/__DEFINES/span.dm | 1 + code/__HELPERS/logging/_logging.dm | 2 +- code/__HELPERS/mobs.dm | 2 +- code/datums/chatmessage.dm | 2 +- code/modules/admin/admin_verbs.dm | 46 +- code/modules/admin/verbs/adminhelp.dm | 4 +- code/modules/admin/verbs/adminpm.dm | 915 +++++++++++++++++--------- code/modules/admin/verbs/pray.dm | 2 +- code/modules/emoji/emoji_parse.dm | 2 + 9 files changed, 653 insertions(+), 323 deletions(-) diff --git a/code/__DEFINES/span.dm b/code/__DEFINES/span.dm index e4ff9f02e25..c9e5914d8d4 100644 --- a/code/__DEFINES/span.dm +++ b/code/__DEFINES/span.dm @@ -59,6 +59,7 @@ #define span_info(str) ("" + str + "") #define span_infoplain(str) ("" + str + "") #define span_interface(str) ("" + str + "") +#define span_linkify(str) ("" + str + "") #define span_looc(str) ("" + str + "") #define span_medal(str) ("" + str + "") #define span_medradio(str) ("" + str + "") diff --git a/code/__HELPERS/logging/_logging.dm b/code/__HELPERS/logging/_logging.dm index 70211522577..2cc03731920 100644 --- a/code/__HELPERS/logging/_logging.dm +++ b/code/__HELPERS/logging/_logging.dm @@ -207,7 +207,7 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) if(key) if(C?.holder && C.holder.fakekey && !include_name) if(include_link) - . += "" + . += "" . += "Administrator" else if(include_link) diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index 32aab96855a..ab3393d1c29 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -524,7 +524,7 @@ GLOBAL_LIST_EMPTY(species_list) // Displays a message in deadchat, sent by source. source is not linkified, message is, to avoid stuff like character names to be linkified. // Automatically gives the class deadsay to the whole message (message + source) /proc/deadchat_broadcast(message, source=null, mob/follow_target=null, turf/turf_target=null, speaker_key=null, message_type=DEADCHAT_REGULAR, admin_only=FALSE) - message = span_deadsay("[source][message]") + message = span_deadsay("[source][span_linkify(message)]") for(var/mob/M in GLOB.player_list) var/chat_toggles = TOGGLES_DEFAULT_CHAT diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm index 599c4cd6d7f..fb86da85c2c 100644 --- a/code/datums/chatmessage.dm +++ b/code/datums/chatmessage.dm @@ -32,7 +32,7 @@ * Datum for generating a message overlay on the map */ /datum/chatmessage - /// The visual element of the chat messsage + /// The visual element of the chat message var/image/message /// The location in which the message is appearing var/atom/message_loc diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 59f8f60d04e..54fea48d398 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -407,24 +407,38 @@ GLOBAL_PROTECT(admin_verbs_poll) holder.poll_list_panel() SSblackbox.record_feedback("tally", "admin_verb", 1, "Server Poll Management") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/findStealthKey(txt) - if(txt) - for(var/P in GLOB.stealthminID) - if(GLOB.stealthminID[P] == txt) - return P - txt = GLOB.stealthminID[ckey] - return txt +/// Returns this client's stealthed ckey +/client/proc/getStealthKey() + return GLOB.stealthminID[ckey] + +/// Takes a stealthed ckey as input, returns the true key it represents +/proc/findTrueKey(stealth_key) + if(!stealth_key) + return + for(var/potentialKey in GLOB.stealthminID) + if(GLOB.stealthminID[potentialKey] == stealth_key) + return potentialKey + +/// Hands back a stealth ckey to use, guarenteed to be unique +/proc/generateStealthCkey() + var/guess = rand(0, 1000) + var/text_guess + var/valid_found = FALSE + while(valid_found == FALSE) + valid_found = TRUE + text_guess = "@[num2text(guess)]" + // We take a guess at some number, and if it's not in the existing stealthmin list we exit + for(var/key in GLOB.stealthminID) + // If it is in the list tho, we up one number, and redo the loop + if(GLOB.stealthminID[key] == text_guess) + guess += 1 + valid_found = FALSE + break + + return text_guess /client/proc/createStealthKey() - var/num = (rand(0,1000)) - var/i = 0 - while(i == 0) - i = 1 - for(var/P in GLOB.stealthminID) - if(num == GLOB.stealthminID[P]) - num++ - i = 0 - GLOB.stealthminID["[ckey]"] = "@[num2text(num)]" + GLOB.stealthminID["[ckey]"] = generateStealthCkey() /client/proc/stealth() set category = "Admin" diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 73d3913edcc..fed092305bc 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -416,7 +416,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN)) var/ref_src = "[REF(src)]" //Message to be sent to all admins - var/admin_msg = span_adminnotice(span_adminhelp("Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]: [keywords_lookup(msg)]")) + var/admin_msg = span_adminnotice(span_adminhelp("Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]: [span_linkify(keywords_lookup(msg))]")) AddInteraction("[LinkedReplyName(ref_src)]: [msg]", player_message = "[LinkedReplyName(ref_src)]: [msg]") log_admin_private("Ticket #[id]: [key_name(initiator)]: [msg]") @@ -434,7 +434,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) //show it to the person adminhelping too to_chat(initiator, type = MESSAGE_TYPE_ADMINPM, - html = span_adminnotice("PM to-Admins: [msg]"), + html = span_adminnotice("PM to-Admins: [span_linkify(msg)]"), confidential = TRUE) SSblackbox.LogAhelp(id, "Ticket Opened", msg, null, initiator.ckey, urgent = urgent) diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index a8bb7cb3b9e..07e0bfad116 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -1,6 +1,19 @@ #define EXTERNALREPLYCOUNT 2 +#define EXTERNAL_PM_USER "IRCKEY" -//allows right clicking mobs to send an admin PM to their client, forwards the selected mob's client to cmd_admin_pm +// HEY FUCKO, IMPORTANT NOTE! +// This file, and pretty much everything that directly handles ahelps, is VERY important +// An admin pm dropping by coding error is disastorus, because it gives no feedback to admins, so they think they're being ignored +// It is imparitive that this does not happen. Therefore, runtimes are not allowed in this file +// Additionally, any runtimes here would cause admin tickets to leak into the runtime logs +// This is less of a big deal, but still bad +// +// In service of this goal of NO RUNTIMES then, we make ABSOLUTELY sure to never trust the nullness of a value +// That's why variables are so separated from logic here. It's not a good pattern typically, but it helps make assumptions clear here +// We also make SURE to fail loud, IE: if something stops the message from reaching the recipient, the sender HAS to know +// If you "refactor" this to make it "cleaner" I will send you to hell + +/// Allows right clicking mobs to send an admin PM to their client, forwards the selected mob's client to cmd_admin_pm /client/proc/cmd_admin_pm_context(mob/M in GLOB.mob_list) set category = null set name = "Admin PM Mob" @@ -10,12 +23,16 @@ html = span_danger("Error: Admin-PM-Context: Only administrators may use this command."), confidential = TRUE) return - if(!ismob(M) || !M.client) + if(!ismob(M)) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Context: Target mob is not a mob, somehow."), + confidential = TRUE) return - cmd_admin_pm(M.client,null) + cmd_admin_pm(M.client, null) SSblackbox.record_feedback("tally", "admin_verb", 1, "Admin PM Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -//shows a list of clients we could send PMs to, then forwards our choice to cmd_admin_pm +/// Shows a list of clients we could send PMs to, then forwards our choice to cmd_admin_pm /client/proc/cmd_admin_pm_panel() set category = "Admin" set name = "Admin PM" @@ -25,80 +42,123 @@ html = span_danger("Error: Admin-PM-Panel: Only administrators may use this command."), confidential = TRUE) return + var/list/targets = list() - for(var/client/client as anything in GLOB.clients) - if(client.mob) - if(isnewplayer(client.mob)) - targets["(New Player) - [client]"] = client - else if(isobserver(client.mob)) - targets["[client.mob.name](Ghost) - [client]"] = client - else - targets["[client.mob.real_name](as [client.mob.name]) - [client]"] = client + for(var/client/client in GLOB.clients) + var/nametag = "" + var/mob/lad = client.mob + var/mob_name = lad?.name + var/real_mob_name = lad?.real_name + if(!lad) + nametag = "(No Mob)" + else if(isnewplayer(lad)) + nametag = "(New Player)" + else if(isobserver(lad)) + nametag = "[mob_name](Ghost)" else - targets["(No Mob) - [client]"] = client - var/target = input(src,"To whom shall we send a message?","Admin PM",null) as null|anything in sort_list(targets) - cmd_admin_pm(targets[target],null) + nametag = "[real_mob_name](as [mob_name])" + targets["[nametag] - [client]"] = client + + var/target = input(src,"To whom shall we send a message?", "Admin PM", null) as null|anything in sort_list(targets) + cmd_admin_pm(targets[target], null) SSblackbox.record_feedback("tally", "admin_verb", 1, "Admin PM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! +/// Replys to some existing ahelp, reply to whom, which can be a client or ckey /client/proc/cmd_ahelp_reply(whom) + if(IsAdminAdvancedProcCall()) + return FALSE + if(prefs.muted & MUTE_ADMINHELP) to_chat(src, type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Error: Admin-PM: You are unable to use admin PM-s (muted)."), + html = span_danger("Error: Admin-PM-Reply: You are unable to use admin PM-s (muted)."), confidential = TRUE) return - var/client/C - if(istext(whom)) - if(whom[1] == "@") - whom = findStealthKey(whom) - C = GLOB.directory[whom] - else if(istype(whom, /client)) - C = whom - if(!C) + + // We use the ckey here rather then keeping the client to ensure resistance to client logouts mid execution + if(istype(whom, /client)) + var/client/boi = whom + whom = boi.ckey + + var/ambiguious_recipient = disambiguate_client(whom) + if(!istype(ambiguious_recipient, /client)) if(holder) to_chat(src, type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Error: Admin-PM: Client not found."), + html = span_danger("Error: Admin-PM-Reply: Client not found."), confidential = TRUE) return - var/datum/admin_help/AH = C.current_ticket + // Existing client case + var/client/recipient = ambiguious_recipient + + // The ticket our recipient is using + var/datum/admin_help/recipient_ticket = recipient?.current_ticket + // Any past interactions with the recipient ticket + var/datum/admin_help/recipient_interactions = recipient_ticket?.ticket_interactions + // Any opening interactions with the recipient ticket, IE: interactions started before the ticket first recieves a response + var/datum/admin_help/opening_interactions = recipient_ticket?.opening_responders + // Our recipient's admin holder, if one exists + var/datum/admins/recipient_holder = recipient?.holder + // The ckey of our recipient + var/recipient_ckey = recipient?.ckey + // Our recipient's fake key, if they are faking their ckey + var/recipient_fake_key = recipient_holder?.fakekey + // Our ckey, with our mob's name if one exists, formatted with a reply link + var/our_linked_name = key_name_admin(src) + // The recipient's ckey, formatted with a reply link + var/recipient_linked_ckey = key_name_admin(recipient, FALSE) + // The recipient's ckey, formatted slightly with html + var/formatted_recipient_ckey = key_name(recipient, FALSE, FALSE) var/message_prompt = "Message:" + if(recipient_ticket) + message_admins("[our_linked_name] has started replying to [recipient_linked_ckey]'s admin help.") + // If none's interacted with the ticket yet + if(length(recipient_interactions) == 1) + if(length(opening_interactions)) // Inform the admin that they aren't the first + var/printable_interators = english_list(opening_interactions) + SEND_SOUND(src, sound('sound/machines/buzz-sigh.ogg', volume=30)) + message_prompt += "\n\n**This ticket is already being responded to by: [printable_interators]**" + // add the admin who is currently responding to the list of people responding + LAZYADD(recipient_ticket.opening_responders, src) - if(AH?.opening_responders && length(AH.ticket_interactions) == 1) - SEND_SOUND(src, sound('sound/machines/buzz-sigh.ogg', volume=30)) - message_prompt += "\n\n**This ticket is already being responded to by: [english_list(AH.opening_responders)]**" + var/request = "Private message to" + if(recipient_fake_key) + request = "[request] an Administrator." + else + request = "[request] [formatted_recipient_ckey]." - if(AH) - message_admins("[key_name_admin(src)] has started replying to [key_name_admin(C, 0, 0)]'s admin help.") - if(length(AH.ticket_interactions) == 1) // add the admin who is currently responding to the list of people responding - LAZYADD(AH.opening_responders, src) + var/message = input(src, message_prompt, request) as message|null - var/msg = input(src, message_prompt, "Private message to [C.holder?.fakekey ? "an Administrator" : key_name(C, 0, 0)].") as message|null - LAZYREMOVE(AH.opening_responders, src) - if (!msg) - message_admins("[key_name_admin(src)] has cancelled their reply to [key_name_admin(C, 0, 0)]'s admin help.") + if(recipient_ticket) + LAZYREMOVE(recipient_ticket.opening_responders, src) + + if (!message) + message_admins("[our_linked_name] has cancelled their reply to [recipient_linked_ckey]'s admin help.") return - if(!C) //We lost the client during input, disconnected or relogged. - if(GLOB.directory[AH.initiator_ckey]) // Client has reconnected, lets try to recover - whom = GLOB.directory[AH.initiator_ckey] + + if(!recipient) //We lost the client during input, disconnected or relogged. + if(GLOB.directory[recipient_ckey]) // Client has reconnected, lets try to recover + whom = GLOB.directory[recipient_ckey] else to_chat(src, type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Error: Admin-PM: Client not found."), + html = span_danger("Error: Admin-PM-Reply: Client not found."), confidential = TRUE) to_chat(src, type = MESSAGE_TYPE_ADMINPM, - html = "[span_danger("Message not sent:")]
[msg]", + html = "[span_danger("Message not sent:")]
[message]", confidential = TRUE) - AH.AddInteraction("No client found, message not sent:
[msg]") + if(recipient_ticket) + recipient_ticket.AddInteraction("No client found, message not sent:
[message]") return - cmd_admin_pm(whom, msg) + cmd_admin_pm(whom, message) //takes input from cmd_admin_pm_context, cmd_admin_pm_panel or /client/Topic and sends them a PM. -//Fetching a message if needed. src is the sender and C is the target client -/client/proc/cmd_admin_pm(whom, msg) +//Fetching a message if needed. +//whom here is a client, a ckey, or [EXTERNAL_PM_USER] if this is from tgs. message is the default message to send +/client/proc/cmd_admin_pm(whom, message) if(prefs.muted & MUTE_ADMINHELP) to_chat(src, type = MESSAGE_TYPE_ADMINPM, @@ -113,339 +173,592 @@ confidential = TRUE) to_chat(src, type = MESSAGE_TYPE_ADMINPM, - html = span_notice("Message: [msg]"), + html = span_notice("Message: [message]"), confidential = TRUE) return - var/client/recipient - var/recipient_ckey // Stored in case client is deleted between this and after the message is input - var/datum/admin_help/recipient_ticket // Stored in case client is deleted between this and after the message is input - var/external = 0 - if(istext(whom)) - if(whom[1] == "@") - whom = findStealthKey(whom) - if(whom == "IRCKEY") - external = 1 - else - recipient = GLOB.directory[whom] - else if(istype(whom, /client)) - recipient = whom + // We use the ckey here rather then keeping the client to ensure resistance to client logouts mid execution + if(istype(whom, /client)) + var/client/boi = whom + whom = boi.ckey - if(!recipient) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Error: Admin-PM: Client not found."), - confidential = TRUE) + var/message_to_send = request_adminpm_message(disambiguate_client(whom), message) + if(!message_to_send) return - recipient_ckey = recipient.ckey - recipient_ticket = recipient.current_ticket + if(!sends_adminpm_message(disambiguate_client(whom), message_to_send)) + return - if(external) + notify_adminpm_message(disambiguate_client(whom), message_to_send) + + +/// Requests an admin pm message to send +/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel +/// or a /client, which we will then store info about to ensure logout -> logins are protected as expected +/// Accepts an optional existing message, which will be used in place of asking the recipient assuming all other conditions are met +/// Returns the message to send or null if no message is found +/// Sleeps +/client/proc/request_adminpm_message(ambiguious_recipient, existing_message = null) + if(IsAdminAdvancedProcCall()) + return null + + if(ambiguious_recipient == EXTERNAL_PM_USER) if(!externalreplyamount) //to prevent people from spamming irc/discord - return - if(!msg) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Message: External reply cap hit."), + confidential = TRUE) + return null + var/msg = "" + if(existing_message) + msg = existing_message + else msg = input(src,"Message:", "Private message to Administrator") as message|null if(!msg) - return + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Message: No message input."), + confidential = TRUE) + return null + if(holder) to_chat(src, type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Error: Use the admin IRC/Discord channel, nerd."), + html = span_danger("Error: Admin-PM-Message: Use the admin IRC/Discord channel, nerd."), confidential = TRUE) - return + return null + return msg + if(!istype(ambiguious_recipient, /client)) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Message: Client not found."), + confidential = TRUE) + return null + var/client/recipient = ambiguious_recipient + // Stored in case client is deleted between this and after the message is input + var/recipient_ckey = recipient?.ckey + // Stored in case client is deleted between this and after the message is input + var/datum/admin_help/recipient_ticket = recipient?.current_ticket + // Our current active ticket + var/datum/admin_help/our_ticket = current_ticket + // If our recipient is an admin, this is their admins datum + var/datum/admins/recipient_holder = recipient?.holder + // If our recipient has a fake name, this is it + var/recipient_fake_key = recipient_holder?.fakekey + // Just the recipient's ckey, formatted for htmlifying stuff + var/recipient_print_key = key_name(recipient, FALSE, FALSE) + + // The message we intend on returning + var/msg = "" + if(existing_message) + msg = existing_message else + var/request = "Private message to" + if(recipient_fake_key) + request = "[request] an Administrator." + else + request = "[request] [recipient_print_key]." //get message text, limit it's length.and clean/escape html - if(!msg) - msg = input(src,"Message:", "Private message to [recipient.holder?.fakekey ? "an Administrator" : key_name(recipient, 0, 0)].") as message|null - msg = trim(msg) - if(!msg) - return + msg = input(src,"Message:", request) as message|null + msg = trim(msg) - if(!recipient) - if(GLOB.directory[recipient_ckey]) // Client has reconnected, lets try to recover - recipient = GLOB.directory[recipient_ckey] - else - if(holder) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Error: Admin-PM: Client not found."), - confidential = TRUE) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = "[span_danger("Message not sent:")]
[msg]", - confidential = TRUE) - if(recipient_ticket) - recipient_ticket.AddInteraction("No client found, message not sent:
[msg]") - return - else - current_ticket.MessageNoRecipient(msg) - return + if(!msg) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Message: No message input."), + confidential = TRUE) + return null + if(recipient) + return msg + // Client has disappeared due to logout + if(GLOB.directory[recipient_ckey]) // Client has reconnected, lets try to recover + recipient = GLOB.directory[recipient_ckey] + return msg + // We don't tell standard users if a ticket drops because admins have a way to actually see + // Past tickets, and well, admins are the ones who might ban you if you ignore them + if(holder) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Message: Client not found."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = "[span_danger("Message not sent:")]
[msg]", + confidential = TRUE) + if(recipient_ticket) + recipient_ticket.AddInteraction("No client found, message not sent:
[msg]") + return null + if(our_ticket) + our_ticket.MessageNoRecipient(msg) + return null + +/// Sends a pm message via the tickets system +/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel +/// or a /client, in which case we send in the standard form +/// send_message is the raw message to send, it will be filtered and treated to ensure we do not break any text handling +/// Returns FALSE if the send failed, TRUE otherwise +/client/proc/sends_adminpm_message(ambiguious_recipient, send_message) + if(IsAdminAdvancedProcCall()) + return FALSE + + send_message = adminpm_filter_text(ambiguious_recipient, send_message) + if(!send_message) + return null + + if (handle_spam_prevention(send_message, MUTE_ADMINHELP)) + // handle_spam_prevention does its own "hey buddy ya fucker up here's what happen" + return FALSE + + var/raw_message = send_message + + if(holder) + send_message = emoji_parse(send_message) + + var/keyword_parsed_msg = keywords_lookup(send_message) + // Stores a bit of html with our ckey, name, and a linkified string to click and rely to us with + var/name_key_with_link = key_name(src, TRUE, TRUE) + + if(ambiguious_recipient == EXTERNAL_PM_USER) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_notice("PM to-Admins: [span_linkify(raw_message)]"), + confidential = TRUE) + var/datum/admin_help/new_admin_help = admin_ticket_log(src, + "Reply PM from-[name_key_with_link] to External: [keyword_parsed_msg]", + player_message = "Reply PM from-[name_key_with_link] to External: [send_message]") + var/new_help_id = new_admin_help?.id + + externalreplyamount-- + + var/category = "Reply: [ckey]" + if(new_admin_help) + category = "#[new_help_id] [category]" + + send2adminchat(category, raw_message) + return TRUE + + if(!istype(ambiguious_recipient, /client)) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Send: Client not found."), + confidential = TRUE) + return FALSE + + var/client/recipient = ambiguious_recipient + var/datum/admins/recipient_holder = recipient.holder + var/datum/admins/our_holder = holder + + // The sound preferences of the recipient, at least that's how we'll be using it here + var/sound_prefs = recipient?.prefs?.toggles + // Stores a bit of html that contains the ckey of the recipient, its mob's name if any exist, and a link to reply to them with + var/their_name_with_link = key_name(recipient, TRUE, TRUE) + // Stores a bit of html with our ckey highlighted as a reply link + var/link_to_us = key_name(src, TRUE, FALSE) + // Stores a bit of html with outhe ckey of the recipientr highlighted as a reply link + var/link_to_their = key_name(recipient, TRUE, FALSE) + // Our ckey + var/our_ckey = ckey + // Recipient ckey + var/recip_ckey = recipient?.ckey + // Our current ticket, can (supposedly) be null here + var/datum/admin_help/ticket = current_ticket + // The recipient's current ticket, could in theory? maybe? be null here + var/datum/admin_help/recipient_ticket = recipient?.current_ticket + // I use -1 as a default for both of these + // Our ticket ID + var/ticket_id = ticket?.id + // The recipient's ticket id + var/recipient_ticket_id = recipient_ticket?.id + + // If we should do a full on boink, so with the text and extra flair and everything + // We want to always do this so long as WE are an admin, and we're messaging the "loser" of the converstation + var/full_boink = FALSE + // Only admins can perform boinks + if(our_holder) + full_boink = TRUE + // Tickets will only generate for the non admin/admin being boinked. This check is to ensure boinked admins don't send the same + // ADMINISTRAITOR PRIVATE MESSAGE text to their boinker every time they respond + if(recipient_holder && ticket) + full_boink = FALSE + + // If we're gonna boink em, do it now + // It is worth noting this will always generate the target a ticket if they don't already have one (tickets will generate if a player ahelps automatically, outside this logic) + // So past this point, because of our block above here, we can be reasonably guarenteed that the user will have a ticket + if(full_boink) + // Do BIG RED TEXT + var/already_logged = FALSE + // Full boinks will always be done to players, so we are not guarenteed that they won't have a ticket + if(!recipient_ticket) + new /datum/admin_help(send_message, recipient, TRUE) + already_logged = TRUE + // This action mutates our existing cached ticket information, so we recache + ticket = current_ticket + recipient_ticket = recipient?.current_ticket + ticket_id = ticket?.id + recipient_ticket_id = recipient_ticket?.id + SSblackbox.LogAhelp(recipient_ticket_id, "Ticket Opened", send_message, recipient.ckey, src.ckey) + + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = "-- Administrator private message --", + confidential = TRUE) + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = span_adminsay("Admin PM from-[link_to_us]: [span_linkify(send_message)]"), + confidential = TRUE) + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = span_adminsay("Click on the administrator's name to reply."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_notice("Admin PM to-[their_name_with_link]: [span_linkify(send_message)]"), + confidential = TRUE) + + admin_ticket_log(recipient, + "PM From [name_key_with_link]: [keyword_parsed_msg]", + log_in_blackbox = FALSE, + player_message = "PM From [link_to_us]: [send_message]") + + if(!already_logged) //Reply to an existing ticket + SSblackbox.LogAhelp(recipient_ticket_id, "Reply", send_message, recip_ckey, our_ckey) + + //always play non-admin recipients the adminhelp sound + SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) + return TRUE + + // Ok if we're here, either this message is for an admin, or someone somehow figured out how to send a new message as a player + // First case well, first + if(!our_holder && !recipient_holder) //neither are admins + if(!ticket) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Send: Non-admin to non-admin PM communication is forbidden."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = "[span_danger("Message not sent:")]
[send_message]", + confidential = TRUE) + return FALSE + ticket.MessageNoRecipient(send_message) + return TRUE + + // Ok by this point the recipient has to be an admin, and this is either an admin on admin event, or a player replying to an admin + + // Let's play some music for the admin, only if they want it tho + if(sound_prefs & SOUND_ADMINHELP) + SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) + + SEND_SIGNAL(ticket, COMSIG_ADMIN_HELP_REPLIED) + + // Admin on admin violence first + if(our_holder) + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Admin PM from-[name_key_with_link]: [span_linkify(keyword_parsed_msg)]"), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_notice("Admin PM to-[their_name_with_link]: [span_linkify(keyword_parsed_msg)]"), + confidential = TRUE) + + //omg this is dumb, just fill in both their logs + var/interaction_message = "PM from-[name_key_with_link] to-[their_name_with_link]: [keyword_parsed_msg]" + var/player_interaction_message = "PM from-[link_to_us] to-[link_to_their]: [send_message]" + admin_ticket_log(src, + interaction_message, + log_in_blackbox = FALSE, + player_message = player_interaction_message) + if(recipient != src) //reeee + admin_ticket_log(recipient, + interaction_message, + log_in_blackbox = FALSE, + player_message = player_interaction_message) + + SSblackbox.LogAhelp(ticket_id, "Reply", send_message, recip_ckey, our_ckey) + return TRUE + + // This is us (a player) trying to talk to the recipient (an admin) + var/replymsg = "Reply PM from-[name_key_with_link]: [span_linkify(keyword_parsed_msg)]" + var/player_replymsg = "Reply PM from-[link_to_us]: [span_linkify(send_message)]" + admin_ticket_log(src, + "[replymsg]", + log_in_blackbox = FALSE, + player_message = player_replymsg) + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("[replymsg]"), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_notice("PM to-Admins: [span_linkify(send_message)]"), + confidential = TRUE) + SSblackbox.LogAhelp(ticket_id, "Reply", send_message, recip_ckey, our_ckey) + return TRUE + +/// Notifies all admins about the existance of an admin pm, then logs the pm +/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel +/// or a /client, in which case we send in the standard form +/// log_message is the raw message to send, it will be filtered and treated to ensure we do not break any text handling +/client/proc/notify_adminpm_message(ambiguious_recipient, log_message) + if(IsAdminAdvancedProcCall()) + return + + // First we filter, because these procs can be called by anyone with debug, and I don't trust that check + // gotta make sure none's fucking about + log_message = adminpm_filter_text(ambiguious_recipient, log_message) + if(!log_message) + return + + var/raw_message = log_message + + if(holder) + log_message = emoji_parse(log_message) + + var/keyword_parsed_msg = keywords_lookup(log_message) + // Shows our ckey and the name of any mob we might be possessing + var/our_name = key_name(src) + // Shows our ckey/name embedded inside a clickable link to reply to this message + var/our_linked_ckey = key_name(src, TRUE, FALSE) + // Our current active ticket + var/datum/admin_help/ticket = current_ticket + // Our current ticket id, if one exists + var/ticket_id = ticket?.id + + if(ambiguious_recipient == EXTERNAL_PM_USER) + // Guard against the possibility of a null, since it'll runtime and spit out the contents of what should be a private ticket. + if(ticket) + log_admin_private("PM: Ticket #[ticket_id]: [our_name]->External: [raw_message]") + else + log_admin_private("PM: [our_name]->External: [raw_message]") + for(var/client/lad in GLOB.admins) + to_chat(lad, + type = MESSAGE_TYPE_ADMINPM, + html = span_notice("PM: [our_linked_ckey]->External: [keyword_parsed_msg]"), + confidential = TRUE) + return + if(!istype(ambiguious_recipient, /client)) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = span_danger("Error: Admin-PM-Notify: Client not found."), + confidential = TRUE) + return + + var/client/recipient = ambiguious_recipient + // The ckey of our recipient + var/recipient_key = recipient?.ckey + // Shows the recipient's ckey and the name of any mob it might be possessing + var/recipient_name = key_name(recipient) + // Shows the recipient's ckey/name embedded inside a clickable link to reply to this message + var/recipient_linked_ckey = key_name(recipient, TRUE, FALSE) + + window_flash(recipient, ignorepref = TRUE) + if(ticket) + log_admin_private("PM: Ticket #[ticket_id]: [our_name]->[recipient_name]: [raw_message]") + else + log_admin_private("PM: [our_name]->[recipient_name]: [raw_message]") + //we don't use message_admins here because the sender/receiver might get it too + for(var/client/lad in GLOB.admins) + if(lad.key == key || lad.key == recipient_key) //check to make sure client/lad isn't the sender or recipient + continue + to_chat(lad, + type = MESSAGE_TYPE_ADMINPM, + html = span_notice("PM: [our_linked_ckey]->[recipient_linked_ckey]: [keyword_parsed_msg]") , + confidential = TRUE) + +/// Accepts a message and an ambiguious recipient (some sort of client representative, or [EXTERNAL_PM_USER]) +/// Returns the filtered message if it passes all checks, or null if the send fails +/client/proc/adminpm_filter_text(ambiguious_recipient, message) if(prefs.muted & MUTE_ADMINHELP) to_chat(src, type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Error: Admin-PM: You are unable to use admin PM-s (muted)."), + html = span_danger("Error: Admin-PM-Filter: You are unable to use admin PM-s (muted)."), confidential = TRUE) return - if (src.handle_spam_prevention(msg,MUTE_ADMINHELP)) - return - //clean the message if it's not sent by a high-rank admin - if(!check_rights(R_SERVER|R_DEBUG,0)||external)//no sending html to the poor bots - msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN)) - if(!msg) - return - - var/rawmsg = msg - - if(holder) - msg = emoji_parse(msg) - - var/keywordparsedmsg = keywords_lookup(msg) - - if(external) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = span_notice("PM to-Admins: [rawmsg]"), - confidential = TRUE) - var/datum/admin_help/new_admin_help = admin_ticket_log(src, "Reply PM from-[key_name(src, TRUE, TRUE)] to External: [keywordparsedmsg]", player_message = "Reply PM from-[key_name(src, TRUE, FALSE)] to External: [msg]") - externalreplyamount-- - send2adminchat("[new_admin_help ? "#[new_admin_help.id] " : ""]Reply: [ckey]", rawmsg) - else - var/badmin = FALSE //Lets figure out if an admin is getting bwoinked. - if(holder && recipient.holder && !current_ticket) //Both are admins, and this is not a reply to our own ticket. - badmin = TRUE - if(recipient.holder && !badmin) - SEND_SIGNAL(current_ticket, COMSIG_ADMIN_HELP_REPLIED) - - if(holder) - to_chat(recipient, - type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Admin PM from-[key_name(src, recipient, 1)]: [keywordparsedmsg]"), - confidential = TRUE) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = span_notice("Admin PM to-[key_name(recipient, src, 1)]: [keywordparsedmsg]"), - confidential = TRUE) - //omg this is dumb, just fill in both their tickets - var/interaction_message = "PM from-[key_name(src, recipient, TRUE)] to-[key_name(recipient, src, TRUE)]: [keywordparsedmsg]" - var/player_interaction_message = "PM from-[key_name(src, recipient, FALSE)] to-[key_name(recipient, src, FALSE)]: [msg]" - admin_ticket_log(src, interaction_message, log_in_blackbox = FALSE, player_message = player_interaction_message) - if(recipient != src) //reeee - admin_ticket_log(recipient, interaction_message, log_in_blackbox = FALSE, player_message = player_interaction_message) - SSblackbox.LogAhelp(current_ticket.id, "Reply", msg, recipient.ckey, src.ckey) - else //recipient is an admin but sender is not - var/replymsg = "Reply PM from-[key_name(src, recipient, TRUE)]: [keywordparsedmsg]" - var/player_replymsg = "Reply PM from-[key_name(src, recipient, FALSE)]: [msg]" - admin_ticket_log(src, "[replymsg]", log_in_blackbox = FALSE, player_message = player_replymsg) - to_chat(recipient, - type = MESSAGE_TYPE_ADMINPM, - html = span_danger("[replymsg]"), - confidential = TRUE) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = span_notice("PM to-Admins: [msg]"), - confidential = TRUE) - SSblackbox.LogAhelp(current_ticket.id, "Reply", msg, recipient.ckey, src.ckey) - - //play the receiving admin the adminhelp sound (if they have them enabled) - if(recipient.prefs.toggles & SOUND_ADMINHELP) - SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) - else - if(holder) //sender is an admin but recipient is not. Do BIG RED TEXT - var/already_logged = FALSE - if(!recipient.current_ticket) - new /datum/admin_help(msg, recipient, TRUE, src) //SKYRAT EDIT CHANGE - ADMIN - already_logged = TRUE - SSblackbox.LogAhelp(recipient.current_ticket.id, "Ticket Opened", msg, recipient.ckey, src.ckey) - - //SKYRAT EDIT ADDITION BEGIN - ADMIN - // Basically, if we realized that we shouldn't've been handling the ticket, let's bail. Otherwise, we just change who's handling it. - if(!recipient.current_ticket.handle_issue()) - return - // SKYRAT EDIT END - - to_chat(recipient, - type = MESSAGE_TYPE_ADMINPM, - html = "-- Administrator private message --", - confidential = TRUE) - to_chat(recipient, - type = MESSAGE_TYPE_ADMINPM, - html = span_adminsay("Admin PM from-[key_name(src, recipient, 0)]: [msg]"), - confidential = TRUE) - to_chat(recipient, - type = MESSAGE_TYPE_ADMINPM, - html = span_adminsay("Click on the administrator's name to reply."), - confidential = TRUE) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = span_notice("Admin PM to-[key_name(recipient, src, 1)]: [msg]"), - confidential = TRUE) - - admin_ticket_log(recipient, "PM From [key_name_admin(src)]: [keywordparsedmsg]", log_in_blackbox = FALSE, player_message = "PM From [key_name_admin(src, include_name = FALSE)]: [msg]") - - if(!already_logged) //Reply to an existing ticket - SSblackbox.LogAhelp(recipient.current_ticket.id, "Reply", msg, recipient.ckey, src.ckey) - - //always play non-admin recipients the adminhelp sound - SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) - - else //neither are admins - if(!current_ticket) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = span_danger("Error: Admin-PM: Non-admin to non-admin PM communication is forbidden."), - confidential = TRUE) - to_chat(src, - type = MESSAGE_TYPE_ADMINPM, - html = "[span_danger("Message not sent:")]
[msg]", - confidential = TRUE) - return - current_ticket.MessageNoRecipient(msg) - - if(external) - // Guard against the possibility of a null, since it'll runtime and spit out the contents of what should be a private ticket. - if(current_ticket) - log_admin_private("PM: Ticket #[current_ticket.id]: [key_name(src)]->External: [rawmsg]") - else - log_admin_private("PM: [key_name(src)]->External: [rawmsg]") - for(var/client/X in GLOB.admins) - to_chat(X, + if(!check_rights(R_SERVER|R_DEBUG, 0) || ambiguious_recipient == EXTERNAL_PM_USER)//no sending html to the poor bots + message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN)) + if(!message) + to_chat(src, type = MESSAGE_TYPE_ADMINPM, - html = span_notice("PM: [key_name(src, X, 0)]->External: [keywordparsedmsg]"), + html = span_danger("Error: Admin-PM-Filter: Your message contained only HTML, it's been sanitized away and the message disregarded."), confidential = TRUE) - else - window_flash(recipient, ignorepref = TRUE) - if(current_ticket) - log_admin_private("PM: Ticket #[current_ticket.id]: [key_name(src)]->[key_name(recipient)]: [rawmsg]") - else - log_admin_private("PM: [key_name(src)]->[key_name(recipient)]: [rawmsg]") - //we don't use message_admins here because the sender/receiver might get it too - for(var/client/X in GLOB.admins) - if(X.key!=key && X.key!=recipient.key) //check client/X is an admin and isn't the sender or recipient - to_chat(X, - type = MESSAGE_TYPE_ADMINPM, - html = span_notice("PM: [key_name(src, X, 0)]->[key_name(recipient, X, 0)]: [keywordparsedmsg]") , - confidential = TRUE) + return + return message #define TGS_AHELP_USAGE "Usage: ticket " -/proc/TgsPm(target,msg,sender) - target = ckey(target) - var/client/C = GLOB.directory[target] +/proc/TgsPm(target, message, sender) + var/requested_ckey = ckey(target) + var/ambiguious_target = disambiguate_client(requested_ckey) - var/datum/admin_help/ticket = C ? C.current_ticket : GLOB.ahelp_tickets.CKey2ActiveTicket(target) - var/compliant_msg = trim(lowertext(msg)) + var/client/recipient + // This might seem like hiding a failure condition, but we want to be able to send commands to the ticket without the client being logged in + if(istype(ambiguious_target, /client)) + recipient = ambiguious_target + + // The ticket we want to talk about here. Either the target's active ticket, or the last one it had + var/datum/admin_help/ticket + if(recipient) + ticket = recipient.current_ticket + else + GLOB.ahelp_tickets.CKey2ActiveTicket(requested_ckey) + // The ticket's id + var/ticket_id = ticket?.id + + var/compliant_msg = trim(lowertext(message)) var/tgs_tagged = "[sender](TGS/External)" var/list/splits = splittext(compliant_msg, " ") - if(splits.len && splits[1] == "ticket") - if(splits.len < 2) + var/split_size = length(splits) + + if(split_size && splits[1] == "ticket") + if(split_size < 2) return TGS_AHELP_USAGE switch(splits[2]) if("close") if(ticket) ticket.Close(tgs_tagged) - return "Ticket #[ticket.id] successfully closed" + return "Ticket #[ticket_id] successfully closed" if("resolve") if(ticket) ticket.Resolve(tgs_tagged) - return "Ticket #[ticket.id] successfully resolved" + return "Ticket #[ticket_id] successfully resolved" if("icissue") if(ticket) ticket.ICIssue(tgs_tagged) - return "Ticket #[ticket.id] successfully marked as IC issue" + return "Ticket #[ticket_id] successfully marked as IC issue" if("reject") if(ticket) ticket.Reject(tgs_tagged) - return "Ticket #[ticket.id] successfully rejected" + return "Ticket #[ticket_id] successfully rejected" if("reopen") if(ticket) - return "Error: [target] already has ticket #[ticket.id] open" - var/fail = splits.len < 3 ? null : -1 - if(!isnull(fail)) - fail = text2num(splits[3]) - if(isnull(fail)) + return "Error: [target] already has ticket #[ticket_id] open" + var/ticket_num + // If the passed in command actually has a ticket id arg + if(split_size >= 3) + ticket_num = text2num(splits[3]) + + if(isnull(ticket_num)) return "Error: No/Invalid ticket id specified. [TGS_AHELP_USAGE]" - var/datum/admin_help/AH = GLOB.ahelp_tickets.TicketByID(fail) - if(!AH) - return "Error: Ticket #[fail] not found" - if(AH.initiator_ckey != target) - return "Error: Ticket #[fail] belongs to [AH.initiator_ckey]" - AH.Reopen() - return "Ticket #[ticket.id] successfully reopened" + + // The active ticket we're trying to reopen, if one exists + var/datum/admin_help/active_ticket = GLOB.ahelp_tickets.TicketByID(ticket_num) + // The ckey of the player to be targeted BY the ticket + // Not the initiator all the time + var/boinked_ckey = active_ticket?.initiator_ckey + + if(!active_ticket) + return "Error: Ticket #[ticket_num] not found" + if(boinked_ckey != target) + return "Error: Ticket #[ticket_num] belongs to [boinked_ckey]" + + active_ticket.Reopen() + return "Ticket #[ticket_num] successfully reopened" if("list") var/list/tickets = GLOB.ahelp_tickets.TicketsByCKey(target) - if(!tickets.len) + var/tickets_length = length(tickets) + + if(!tickets_length) return "None" - . = "" - for(var/I in tickets) - var/datum/admin_help/AH = I - if(.) - . += ", " - if(AH == ticket) - . += "Active: " - . += "#[AH.id]" - return + var/list/printable_tickets = list() + for(var/datum/admin_help/iterated_ticket in tickets) + // The id of the iterated adminhelp + var/iterated_id = iterated_ticket?.id + var/text = "" + if(iterated_ticket == ticket) + text += "Active: " + text += "#[iterated_id]" + printable_tickets += text + return printable_tickets.Join(", ") else return TGS_AHELP_USAGE return "Error: Ticket could not be found" - var/static/stealthkey - var/adminname = CONFIG_GET(flag/show_irc_name) ? tgs_tagged : "Administrator" - - if(!C) + // Now that we've handled command processing, we can actually send messages to the client + if(!recipient) return "Error: No client" - if(!stealthkey) - stealthkey = GenTgsStealthKey() + var/adminname + if(CONFIG_GET(flag/show_irc_name)) + adminname = tgs_tagged + else + adminname = "Administrator" - msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN)) - if(!msg) + var/stealthkey = GetTgsStealthKey() + + message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN)) + message = emoji_parse(message) + + if(!message) return "Error: No message" - message_admins("External message from [sender] to [key_name_admin(C)] : [msg]") - log_admin_private("External PM: [sender] -> [key_name(C)] : [msg]") - msg = emoji_parse(msg) + // The ckey of our recipient, with a reply link, and their mob if one exists + var/recipient_name_linked = key_name_admin(recipient) + // The ckey of our recipient, with their mob if one exists. No link + var/recipient_name = key_name_admin(recipient) - to_chat(C, + message_admins("External message from [sender] to [recipient_name_linked] : [message]") + log_admin_private("External PM: [sender] -> [recipient_name] : [message]") + + to_chat(recipient, type = MESSAGE_TYPE_ADMINPM, html = "-- Administrator private message --", confidential = TRUE) - to_chat(C, + to_chat(recipient, type = MESSAGE_TYPE_ADMINPM, - html = span_adminsay("Admin PM from-
[adminname]: [msg]"), + html = span_adminsay("Admin PM from-[adminname]: [message]"), confidential = TRUE) - to_chat(C, + to_chat(recipient, type = MESSAGE_TYPE_ADMINPM, html = span_adminsay("Click on the administrator's name to reply."), confidential = TRUE) - admin_ticket_log(C, "PM From [tgs_tagged]: [msg]", log_in_blackbox = FALSE) + admin_ticket_log(recipient, "PM From [tgs_tagged]: [message]", log_in_blackbox = FALSE) - window_flash(C, ignorepref = TRUE) - //always play non-admin recipients the adminhelp sound - SEND_SOUND(C, 'sound/effects/adminhelp.ogg') - - C.externalreplyamount = EXTERNALREPLYCOUNT + window_flash(recipient, ignorepref = TRUE) + // Nullcheck because we run a winset in window flash and I do not trust byond + if(recipient) + //always play non-admin recipients the adminhelp sound + SEND_SOUND(recipient, 'sound/effects/adminhelp.ogg') + recipient.externalreplyamount = EXTERNALREPLYCOUNT return "Message Successful" -/proc/GenTgsStealthKey() - var/num = (rand(0,1000)) - var/i = 0 - while(i == 0) - i = 1 - for(var/P in GLOB.stealthminID) - if(num == GLOB.stealthminID[P]) - num++ - i = 0 - var/stealth = "@[num2text(num)]" - GLOB.stealthminID["IRCKEY"] = stealth - return stealth +/// Gets TGS's stealth key, generates one if none is found +/proc/GetTgsStealthKey() + var/static/tgsStealthKey + if(tgsStealthKey) + return tgsStealthKey + tgsStealthKey = generateStealthCkey() + GLOB.stealthminID[EXTERNAL_PM_USER] = tgsStealthKey + return tgsStealthKey + +/// Takes an argument which could be either a ckey, /client, or IRC marker, and returns a client if possible +/// Returns [EXTERNAL_PM_USER] if an IRC marker is detected +/// Otherwise returns null +/proc/disambiguate_client(whom) + if(istype(whom, /client)) + return whom + + if(!istext(whom) || !(length(whom) >= 1)) + return null + + var/searching_ckey = whom + if(whom[1] == "@") + searching_ckey = findTrueKey(whom) + + if(searching_ckey == EXTERNAL_PM_USER) + return EXTERNAL_PM_USER + + return GLOB.directory[searching_ckey] + + +#undef EXTERNAL_PM_USER #undef EXTERNALREPLYCOUNT diff --git a/code/modules/admin/verbs/pray.dm b/code/modules/admin/verbs/pray.dm index 944c836a01b..e5458c62a2f 100644 --- a/code/modules/admin/verbs/pray.dm +++ b/code/modules/admin/verbs/pray.dm @@ -41,7 +41,7 @@ var/msg_tmp = msg GLOB.requests.pray(usr.client, msg, usr.job == JOB_CHAPLAIN) - msg = span_adminnotice("[icon2html(cross, GLOB.admins)][prayer_type][deity ? " (to [deity])" : ""]: [ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]: [msg]") + msg = span_adminnotice("[icon2html(cross, GLOB.admins)][prayer_type][deity ? " (to [deity])" : ""]: [ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]: [span_linkify(msg)]") for(var/client/C in GLOB.admins) if(C.prefs.chat_toggles & CHAT_PRAYER) to_chat(C, msg, confidential = TRUE) diff --git a/code/modules/emoji/emoji_parse.dm b/code/modules/emoji/emoji_parse.dm index 16e1eeef6c9..533628eedf4 100644 --- a/code/modules/emoji/emoji_parse.dm +++ b/code/modules/emoji/emoji_parse.dm @@ -1,4 +1,6 @@ /proc/emoji_parse(text) //turns :ai: into an emoji in text. + if(!text) + return text . = text if(!CONFIG_GET(flag/emojis)) return