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