Ahelp and Mhelp QOL (#24597)

* Bound ahelp/mhelp replies more tightly to their ticket.

* Hardened PMs further against missing data.

* Support expanding Discord notifications for ahelp/mhelp.

Functionality changes:
* Informs the user when their ticket is reopened.
* Informs other mentors/admins when tickets are converted.
* Discord messages now get a "Ticket 3, " or similar prefix.
  Not useful by itself, but will be with the following PR.

Code changes:
* When you create a ticket, you get the ticket object back.
* checkForTicket now allows you to fetch a ticket by number, and falls
  back to searching for open tickets if the ticket isn't found.
* Corrected and simplified the logic for adding PMs to tickets.
* Responses are now added through the ticket system, not the tickets
  directly.
This commit is contained in:
Charlie Nolan
2024-03-26 13:44:26 +00:00
committed by GitHub
parent b30890dd2a
commit e2f6212656
10 changed files with 95 additions and 62 deletions
+1
View File
@@ -16,6 +16,7 @@
#define MESSAGE_TYPE_DEADCHAT "deadchat"
#define MESSAGE_TYPE_OOC "ooc"
#define MESSAGE_TYPE_ADMINPM "adminpm"
#define MESSAGE_TYPE_MENTORPM "mentorpm"
#define MESSAGE_TYPE_COMBAT "combat"
#define MESSAGE_TYPE_ADMINCHAT "adminchat"
#define MESSAGE_TYPE_MENTORCHAT "mentorchat"
+16 -4
View File
@@ -110,8 +110,9 @@ SUBSYSTEM_DEF(tickets)
to_chat(C.mob, "<span class='[span_class]'>Your [ticket_name] #[ticketNum] remains open! Visit \"My tickets\" under the Admin Tab to view it.</span>")
var/url_message = makeUrlMessage(C, text, ticketNum)
message_staff(url_message, NONE, TRUE)
return T
else
newTicket(C, text, text)
return newTicket(C, text, text)
/**
* Will add the URLs usable by staff to the message and return it
@@ -126,13 +127,13 @@ SUBSYSTEM_DEF(tickets)
if(istype(target, /datum/ticket))
var/datum/ticket/T = target
M = get_mob_by_ckey(T.client_ckey)
key_and_name = M ? key_name(M, TRUE, ticket_help_type) : "[T.client_ckey] (DC)/(DELETED)"
key_and_name = M ? key_name(M, TRUE, ticket_help_type, ticket_id = T.ticketNum) : "[T.client_ckey] (DC)/(DELETED)"
msg = T.title
ticketNum = T.ticketNum
else if(isclient(target))
var/client/C = target
M = C.mob
key_and_name = key_name(M, TRUE, ticket_help_type)
key_and_name = key_name(M, TRUE, ticket_help_type, ticket_id = ticketNum)
var/list/L = list()
L += "<span class='[ticket_help_span]'>[ticket_help_type]: </span><span class='boldnotice'>[key_and_name] "
@@ -165,12 +166,14 @@ SUBSYSTEM_DEF(tickets)
SEND_SOUND(C, sound('sound/effects/adminticketopen.ogg'))
message_staff(url_title, NONE, TRUE)
return T
//Set ticket state with key N to open
/datum/controller/subsystem/tickets/proc/openTicket(N)
var/datum/ticket/T = allTickets[N]
if(T.ticketState != TICKET_OPEN)
message_staff("<span class='[span_class]'>[usr.client] / ([usr]) re-opened [ticket_name] number [N]</span>")
to_chat_safe(returnClient(N), "<span class='[span_class]'>Your [ticket_name] has been re-opened.</span>")
T.ticketState = TICKET_OPEN
return TRUE
@@ -183,6 +186,12 @@ SUBSYSTEM_DEF(tickets)
to_chat_safe(returnClient(N), "<span class='[span_class]'>Your [ticket_name] has now been resolved.</span>")
return TRUE
/datum/controller/subsystem/tickets/proc/addResponse(list/tickets, who, message)
var/list/ticket_numbers = list()
for(var/datum/ticket/T in tickets)
ticket_numbers += T.ticketNum
T.addResponse(who, message)
/datum/controller/subsystem/tickets/proc/convert_to_other_ticket(ticketId)
if(!check_rights(rights_needed))
return
@@ -195,6 +204,7 @@ SUBSYSTEM_DEF(tickets)
to_chat(usr, "<span class='warning'>This ticket has already been converted!</span>")
return
convert_ticket(T)
message_staff("<span class='[span_class]'>[usr.client] / ([usr]) converted [ticket_name] number [ticketId]</span>")
/datum/controller/subsystem/tickets/proc/other_ticket_system_staff_check()
var/list/staff = staff_countup(other_ticket_permission)
@@ -301,7 +311,9 @@ SUBSYSTEM_DEF(tickets)
return FALSE
//Check if the user has ANY ticket not resolved or closed.
/datum/controller/subsystem/tickets/proc/checkForTicket(client/C)
/datum/controller/subsystem/tickets/proc/checkForTicket(client/C, ticket_id = -1)
if(ticket_id > 0 && ticket_id <= length(allTickets))
return list(allTickets[ticket_id])
var/list/tickets = list()
for(var/datum/ticket/T in allTickets)
if(T.client_ckey == C.ckey && (T.ticketState == TICKET_OPEN || T.ticketState == TICKET_STALE))
+7 -7
View File
@@ -1,11 +1,11 @@
// Always return "Something/(Something)", even if it's an error message.
/proc/key_name(whom, include_link = FALSE, type = null)
return key_name_helper(whom, TRUE, include_link, type)
/proc/key_name(whom, include_link = FALSE, type = null, ticket_id = null)
return key_name_helper(whom, TRUE, include_link, type, ticket_id = ticket_id)
/proc/key_name_hidden(whom, include_link = FALSE, type = null)
return key_name_helper(whom, FALSE, include_link, type)
/proc/key_name_hidden(whom, include_link = FALSE, type = null, ticket_id = null)
return key_name_helper(whom, FALSE, include_link, type, ticket_id = ticket_id)
/proc/key_name_helper(whom, include_name, include_link = FALSE, type = null)
/proc/key_name_helper(whom, include_name, include_link = FALSE, type = null, ticket_id = null)
if(include_link != FALSE && include_link != TRUE)
stack_trace("Key_name was called with an incorrect include_link [include_link]")
@@ -42,11 +42,11 @@
if(key)
if(C && C.holder && C.holder.fakekey && !include_name)
if(include_link)
. += "<a href='?priv_msg=[C.getStealthKey()];type=[type]'>"
. += "<a href='?priv_msg=[C.getStealthKey()];type=[type];ticket_id=[ticket_id]'>"
. += "Administrator"
else
if(include_link && C)
. += "<a href='?priv_msg=[C.ckey];type=[type]'>"
. += "<a href='?priv_msg=[C.ckey];type=[type];ticket_id=[ticket_id]'>"
. += key
// See if the player is on the watchlist. Requires admin permissions.
if(check_rights(R_ADMIN, FALSE) && C && C.watchlisted)
+1 -1
View File
@@ -44,7 +44,7 @@ GLOBAL_VAR_INIT(nologevent, 0)
for(var/client/C in GLOB.admins)
if(check_rights(R_ADMIN | R_MENTOR | R_MOD, 0, C.mob))
if(important || (C.prefs && !(C.prefs.toggles & PREFTOGGLE_CHAT_NO_TICKETLOGS)))
to_chat(C, msg, MESSAGE_TYPE_MENTORCHAT, confidential = TRUE)
to_chat(C, msg, MESSAGE_TYPE_MENTORPM, confidential = TRUE)
if(important)
if(C.prefs?.sound & SOUND_MENTORHELP)
SEND_SOUND(C, sound('sound/effects/adminhelp.ogg'))
+12 -5
View File
@@ -25,13 +25,20 @@
if(!msg) // No message after sanitisation
return
var/span_type
var/message_type
var/datum/ticket/T
if(selected_type == "Mentorhelp")
SSmentor_tickets.newHelpRequest(src, msg) // Mhelp
T = SSmentor_tickets.newHelpRequest(src, msg) // Mhelp
span_type = "mentorhelp"
message_type = MESSAGE_TYPE_MENTORPM
else
SStickets.newHelpRequest(src, msg) // Ahelp
T = SStickets.newHelpRequest(src, msg) // Ahelp
span_type = "adminhelp"
message_type = MESSAGE_TYPE_ADMINPM
//show it to the person adminhelping too
to_chat(src, "<span class='boldnotice'>[selected_type]</b>: [msg]</span>", MESSAGE_TYPE_ADMINPM, confidential = TRUE)
to_chat(src, "<span class='[span_type]'>[selected_type]</b>: [msg]</span>", message_type, confidential = TRUE)
SSblackbox.record_feedback("tally", "admin_verb", 1, "Adminhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
switch(selected_type)
@@ -41,11 +48,11 @@
var/active_admins = admincount[1]
log_admin("[selected_type]: [key_name(src)]: [msg] - heard by [active_admins] non-AFK admins.")
GLOB.discord_manager.send2discord_simple_noadmins("**\[Adminhelp]** [key_name(src)]: [msg]", check_send_always = TRUE)
GLOB.discord_manager.send2discord_simple_noadmins("**\[Adminhelp]** Ticket [T.ticketNum], [key_name(src)]: [msg]", check_send_always = TRUE)
if("Mentorhelp")
var/list/mentorcount = staff_countup(R_MENTOR)
var/active_mentors = mentorcount[1]
log_admin("[selected_type]: [key_name(src)]: [msg] - heard by [active_mentors] non-AFK mentors.")
GLOB.discord_manager.send2discord_simple_mentor("[key_name(src)]: [msg]")
GLOB.discord_manager.send2discord_simple_mentor("Ticket [T.ticketNum], [key_name(src)]: [msg]")
+41 -39
View File
@@ -60,7 +60,7 @@
//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, type = "PM")
/client/proc/cmd_admin_pm(whom, msg, type = "PM", ticket_id = -1)
if(check_mute(ckey, MUTE_ADMINHELP))
to_chat(src, "<span class='danger'>Error: Private-Message: You are unable to use PM-s (muted).</span>")
return
@@ -112,21 +112,30 @@
else
msg = admin_pencode_to_html(msg)
var/recieve_span = "playerreply"
var/send_span
var/recieve_span
var/send_pm_type = " "
var/recieve_pm_type = "Player"
var/message_type
var/datum/controller/subsystem/tickets/tickets_system
// We treat PMs as mentorhelps if we were explicitly so, or if neither
// party is an admin.
if(type == "Mentorhelp" || !(check_rights(R_ADMIN|R_MOD, 0, C.mob) || check_rights(R_ADMIN|R_MOD, 0, src.mob)))
send_span = "mentorhelp"
recieve_span = "mentorhelp"
message_type = MESSAGE_TYPE_MENTORPM
tickets_system = SSmentor_tickets
else
send_span = "adminhelp"
recieve_span = "adminhelp"
message_type = MESSAGE_TYPE_ADMINPM
tickets_system = SStickets
if(holder)
//mod PMs are maroon
//PMs sent from admins and mods display their rank
if(holder)
if(check_rights(R_MOD|R_MENTOR,0) && !check_rights(R_ADMIN,0))
recieve_span = "mentorhelp"
else
recieve_span = "adminhelp"
send_pm_type = holder.rank + " "
recieve_pm_type = holder.rank
send_pm_type = holder.rank + " "
recieve_pm_type = holder.rank
else if(!C.holder)
to_chat(src, "<span class='danger'>Error: Admin-PM: Non-admin to non-admin PM communication is forbidden.</span>")
@@ -159,12 +168,12 @@
var/emoji_msg = "<span class='emoji_enabled'>[msg]</span>"
recieve_message = chat_box_red("<span class='[recieve_span]'>[type] from-<b>[recieve_pm_type] [C.holder ? key_name(src, TRUE, type) : key_name_hidden(src, TRUE, type)]</b>:<br><br>[emoji_msg]</span>")
recieve_message = chat_box_red("<span class='[recieve_span]'>[type] from-<b>[recieve_pm_type] [C.holder ? key_name(src, TRUE, type, ticket_id = ticket_id) : key_name_hidden(src, TRUE, type, ticket_id = ticket_id)]</b>:<br><br>[emoji_msg]</span>")
to_chat(C, recieve_message)
var/ping_link = check_rights(R_ADMIN, 0, mob) ? "(<a href='?src=[pm_tracker.UID()];ping=[C.key]'>PING</a>)" : ""
var/window_link = "(<a href='?src=[pm_tracker.UID()];newtitle=[C.key]'>WINDOW</a>)"
var/alert_link = check_rights(R_ADMIN, FALSE, mob) ? " (<a href='?src=[pm_tracker.UID()];adminalert=[C.mob.UID()]'>ALERT</a>)" : ""
to_chat(src, "<span class='pmsend'>[send_pm_type][type] to-<b>[holder ? key_name(C, TRUE, type) : key_name_hidden(C, TRUE, type)]</b>: [emoji_msg]</span> [ping_link] [window_link][alert_link]", MESSAGE_TYPE_ADMINPM)
to_chat(src, "<span class='[send_span]'>[send_pm_type][type] to-<b>[holder ? key_name(C, TRUE, type, ticket_id = ticket_id) : key_name_hidden(C, TRUE, type, ticket_id = ticket_id)]</b>: [emoji_msg]</span> [ping_link] [window_link][alert_link]", message_type)
/*if(holder && !C.holder)
C.last_pm_recieved = world.time
@@ -182,38 +191,31 @@
if(X == C || X == src)
continue
if(X.key != key && X.key != C.key)
switch(type)
if("Mentorhelp")
if(check_rights(R_ADMIN|R_MOD|R_MENTOR, 0, X.mob))
to_chat(X, "<span class='mentorhelp'>[type]: [key_name(src, TRUE, type)]-&gt;[key_name(C, TRUE, type)]: [emoji_msg]</span>", type = MESSAGE_TYPE_MENTORCHAT)
if("Adminhelp")
if(check_rights(R_ADMIN|R_MOD, 0, X.mob))
to_chat(X, "<span class='adminhelp'>[type]: [key_name(src, TRUE, type)]-&gt;[key_name(C, TRUE, type)]: [emoji_msg]</span>", type = MESSAGE_TYPE_ADMINCHAT)
else
if(check_rights(R_ADMIN|R_MOD, 0, X.mob))
to_chat(X, "<span class='boldnotice'>[type]: [key_name(src, TRUE, type)]-&gt;[key_name(C, TRUE, type)]: [emoji_msg]</span>", type = MESSAGE_TYPE_ADMINCHAT)
if(message_type == MESSAGE_TYPE_MENTORPM)
if(check_rights(R_ADMIN|R_MOD|R_MENTOR, 0, X.mob))
to_chat(X, "<span class='mentorhelp'>[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]-&gt;[key_name(C, TRUE, type, ticket_id = ticket_id)]: [emoji_msg]</span>", type = message_type)
else
if(check_rights(R_ADMIN|R_MOD, 0, X.mob))
to_chat(X, "<span class='adminhelp'>[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]-&gt;[key_name(C, TRUE, type, ticket_id = ticket_id)]: [emoji_msg]</span>", type = message_type)
//Check if the mob being PM'd has any open admin tickets.
var/tickets = list()
if(type == "Mentorhelp")
tickets = SSmentor_tickets.checkForTicket(C)
else
tickets = SStickets.checkForTicket(C)
if(tickets)
for(var/datum/ticket/i in tickets)
i.addResponse(src, msg) // Add this response to their open tickets.
//Check if the mob being PM'd has any open tickets.
var/list/tickets = tickets_system.checkForTicket(C, ticket_id)
if(length(tickets))
tickets_system.addResponse(tickets, src, msg)
return
if(type == "Mentorhelp")
if(check_rights(R_ADMIN|R_MOD|R_MENTOR, 0, C.mob)) //Is the person being pm'd an admin? If so we check if the pm'er has open tickets
// If we didn't find a specific ticket by the target mob, we check for
// tickets by the source mob.
if(message_type == MESSAGE_TYPE_MENTORPM)
if(check_rights(R_ADMIN|R_MOD|R_MENTOR, 0, C.mob))
tickets = SSmentor_tickets.checkForTicket(src)
else // Ahelp
if(check_rights(R_ADMIN|R_MOD, 0, C.mob)) //Is the person being pm'd an admin? If so we check if the pm'er has open tickets
else
if(check_rights(R_ADMIN|R_MOD, 0, C.mob))
tickets = SStickets.checkForTicket(src)
if(tickets)
for(var/datum/ticket/i in tickets)
i.addResponse(src, msg)
return
if(length(tickets))
tickets_system.addResponse(tickets, src, msg)
/client/proc/cmd_admin_discord_pm()
if(check_mute(ckey, MUTE_ADMINHELP))
+1 -1
View File
@@ -105,7 +105,7 @@
if(href_list["priv_msg"])
var/ckey_txt = href_list["priv_msg"]
cmd_admin_pm(ckey_txt, null, href_list["type"])
cmd_admin_pm(ckey_txt, null, href_list["type"], ticket_id = text2num(href_list["ticket_id"]))
return
if(href_list["discord_msg"])
+6 -2
View File
@@ -7,7 +7,7 @@
* Circumvents the message queue and sends the message to the recipient (target) as soon as possible.
* trailing_newline, confidential, and handle_whitespace currently have no effect, please fix this in the future or remove the arguments to lower cache!
*/
/proc/to_chat_immediate(target, html, type, text, avoid_highlighting = FALSE, handle_whitespace = TRUE, trailing_newline = TRUE, confidential = FALSE)
/proc/to_chat_immediate(target, html, type, text, avoid_highlighting = FALSE, handle_whitespace = TRUE, trailing_newline = TRUE, confidential = FALSE, ticket_id = -1)
// Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "")
html = "[html]"
text = "[text]"
@@ -29,6 +29,8 @@
message["html"] = html
if(avoid_highlighting)
message["avoidHighlighting"] = avoid_highlighting
if(ticket_id != -1)
message["ticket_id"] = ticket_id
// send it immediately
SSchat.send_immediate(target, message)
@@ -48,7 +50,7 @@
* AVOID_HIGHLIGHTING: Unused
* trailing_newline, confidential, and handle_whitespace currently have no effect, please fix this in the future or remove the arguments to lower cache!
*/
/proc/to_chat(target, html, type, text, avoid_highlighting, handle_whitespace = TRUE, trailing_newline = TRUE, confidential = FALSE)
/proc/to_chat(target, html, type, text, avoid_highlighting, handle_whitespace = TRUE, trailing_newline = TRUE, confidential = FALSE, ticket_id = -1)
if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized)
to_chat_immediate(target, html, type, text)
return
@@ -74,4 +76,6 @@
message["html"] = html
if(avoid_highlighting)
message["avoidHighlighting"] = avoid_highlighting
if(ticket_id != -1)
message["ticket_id"] = ticket_id
SSchat.queue(target, message)