mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-14 03:32:00 +00:00
* Adds pinging to asay (#61712) I spend a lot of my time adminning the servers alt tabbed when not much is going on, tabbing back in every 5-10 minutes to see what's happening or when I hear a bwoink. Sometimes I miss out on other admins asking me questions or trying to get my attention, and I'll only realize way later when I'm scrolling back up through the chat logs, if at all. This PR adds the ability to @ other admins in asay by their ckey, which underlines the pinged name, plays a bloop sound (the one when a vote starts) and flashes their window icon if they're not tabbed in. * Adds pinging to asay Co-authored-by: Ryll Ryll <3589655+Ryll-Ryll@users.noreply.github.com>
879 lines
31 KiB
Plaintext
879 lines
31 KiB
Plaintext
/// Client var used for returning the ahelp verb
|
|
/client/var/adminhelptimerid = 0
|
|
/// Client var used for tracking the ticket the (usually) not-admin client is dealing with
|
|
/client/var/datum/admin_help/current_ticket
|
|
|
|
GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
|
|
|
/**
|
|
* # Adminhelp Ticket Manager
|
|
*/
|
|
/datum/admin_help_tickets
|
|
/// The set of all active tickets
|
|
var/list/active_tickets = list()
|
|
/// The set of all closed tickets
|
|
var/list/closed_tickets = list()
|
|
/// The set of all resolved tickets
|
|
var/list/resolved_tickets = list()
|
|
|
|
var/obj/effect/statclick/ticket_list/astatclick = new(null, null, AHELP_ACTIVE)
|
|
var/obj/effect/statclick/ticket_list/cstatclick = new(null, null, AHELP_CLOSED)
|
|
var/obj/effect/statclick/ticket_list/rstatclick = new(null, null, AHELP_RESOLVED)
|
|
|
|
/datum/admin_help_tickets/Destroy()
|
|
QDEL_LIST(active_tickets)
|
|
QDEL_LIST(closed_tickets)
|
|
QDEL_LIST(resolved_tickets)
|
|
QDEL_NULL(astatclick)
|
|
QDEL_NULL(cstatclick)
|
|
QDEL_NULL(rstatclick)
|
|
return ..()
|
|
|
|
/datum/admin_help_tickets/proc/TicketByID(id)
|
|
var/list/lists = list(active_tickets, closed_tickets, resolved_tickets)
|
|
for(var/I in lists)
|
|
for(var/J in I)
|
|
var/datum/admin_help/AH = J
|
|
if(AH.id == id)
|
|
return J
|
|
|
|
/datum/admin_help_tickets/proc/TicketsByCKey(ckey)
|
|
. = list()
|
|
var/list/lists = list(active_tickets, closed_tickets, resolved_tickets)
|
|
for(var/I in lists)
|
|
for(var/J in I)
|
|
var/datum/admin_help/AH = J
|
|
if(AH.initiator_ckey == ckey)
|
|
. += AH
|
|
|
|
//private
|
|
/datum/admin_help_tickets/proc/ListInsert(datum/admin_help/new_ticket)
|
|
var/list/ticket_list
|
|
switch(new_ticket.state)
|
|
if(AHELP_ACTIVE)
|
|
ticket_list = active_tickets
|
|
if(AHELP_CLOSED)
|
|
ticket_list = closed_tickets
|
|
if(AHELP_RESOLVED)
|
|
ticket_list = resolved_tickets
|
|
else
|
|
CRASH("Invalid ticket state: [new_ticket.state]")
|
|
var/num_closed = ticket_list.len
|
|
if(num_closed)
|
|
for(var/I in 1 to num_closed)
|
|
var/datum/admin_help/AH = ticket_list[I]
|
|
if(AH.id > new_ticket.id)
|
|
ticket_list.Insert(I, new_ticket)
|
|
return
|
|
ticket_list += new_ticket
|
|
|
|
//opens the ticket listings for one of the 3 states
|
|
/datum/admin_help_tickets/proc/BrowseTickets(state)
|
|
var/list/l2b
|
|
var/title
|
|
switch(state)
|
|
if(AHELP_ACTIVE)
|
|
l2b = active_tickets
|
|
title = "Active Tickets"
|
|
if(AHELP_CLOSED)
|
|
l2b = closed_tickets
|
|
title = "Closed Tickets"
|
|
if(AHELP_RESOLVED)
|
|
l2b = resolved_tickets
|
|
title = "Resolved Tickets"
|
|
if(!l2b)
|
|
return
|
|
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>[title]</title></head>")
|
|
dat += "<A href='?_src_=holder;[HrefToken()];ahelp_tickets=[state]'>Refresh</A><br><br>"
|
|
for(var/I in l2b)
|
|
var/datum/admin_help/AH = I
|
|
dat += "[span_adminnotice("[span_adminhelp("Ticket #[AH.id]")]: <A href='?_src_=holder;[HrefToken()];ahelp=[REF(AH)];ahelp_action=ticket'>[AH.initiator_key_name]: [AH.name]</A>")]<br>"
|
|
|
|
usr << browse(dat.Join(), "window=ahelp_list[state];size=600x480")
|
|
|
|
//Tickets statpanel
|
|
/datum/admin_help_tickets/proc/stat_entry()
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
var/list/L = list()
|
|
var/num_disconnected = 0
|
|
L[++L.len] = list("Active Tickets:", "[astatclick.update("[active_tickets.len]")]", null, REF(astatclick))
|
|
astatclick.update("[active_tickets.len]")
|
|
for(var/I in active_tickets)
|
|
var/datum/admin_help/AH = I
|
|
if(AH.initiator)
|
|
var/obj/effect/statclick/updated = AH.statclick.update()
|
|
//L[++L.len] = list("#[AH.id]. [AH.initiator_key_name]:", "[updated.name]", REF(AH)) //ORIGINAL
|
|
L[++L.len] = list("[AH.handler ? "H-[AH.handler]" : ""]#[AH.id]. [AH.initiator_key_name]:", "[updated.name]", REF(AH)) //SKYRAT EDIT CHANGE - ADMIN
|
|
else
|
|
++num_disconnected
|
|
if(num_disconnected)
|
|
L[++L.len] = list("Disconnected:", "[astatclick.update("[num_disconnected]")]", null, REF(astatclick))
|
|
L[++L.len] = list("Closed Tickets:", "[cstatclick.update("[closed_tickets.len]")]", null, REF(cstatclick))
|
|
L[++L.len] = list("Resolved Tickets:", "[rstatclick.update("[resolved_tickets.len]")]", null, REF(rstatclick))
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN - AMBITIONS
|
|
for(var/key in GLOB.ambitions_to_review)
|
|
var/datum/ambitions/AMBI = key
|
|
var/reviewer = GLOB.ambitions_to_review[key]
|
|
L[++L.len] = list("[reviewer ? "H-[reviewer]. " : ""]AMB: [AMBI.owner_name]:", "[AMBI.narrative]", REF(AMBI))
|
|
L[++L.len] = list("Ambitions intensity:", "STL:[GLOB.intensity_counts["[AMBITION_INTENSITY_STEALTH]"]] MLD:[GLOB.intensity_counts["[AMBITION_INTENSITY_MILD]"]] MED:[GLOB.intensity_counts["[AMBITION_INTENSITY_MEDIUM]"]] SEV:[GLOB.intensity_counts["[AMBITION_INTENSITY_SEVERE]"]] EXT:[GLOB.intensity_counts["[AMBITION_INTENSITY_EXTREME]"]] (HAVOC: [GLOB.total_intensity])", null)
|
|
//SKYRAT EDIT ADDITION END
|
|
return L
|
|
|
|
//Reassociate still open ticket if one exists
|
|
/datum/admin_help_tickets/proc/ClientLogin(client/C)
|
|
C.current_ticket = CKey2ActiveTicket(C.ckey)
|
|
if(C.current_ticket)
|
|
C.current_ticket.initiator = C
|
|
C.current_ticket.AddInteraction("Client reconnected.")
|
|
SSblackbox.LogAhelp(C.current_ticket.id, "Reconnected", "Client reconnected", C.ckey)
|
|
|
|
//Dissasociate ticket
|
|
/datum/admin_help_tickets/proc/ClientLogout(client/C)
|
|
if(C.current_ticket)
|
|
var/datum/admin_help/T = C.current_ticket
|
|
T.AddInteraction("Client disconnected.")
|
|
SSblackbox.LogAhelp(T, "Disconnected", "Client disconnected", C.ckey)
|
|
T.initiator = null
|
|
|
|
//Get a ticket given a ckey
|
|
/datum/admin_help_tickets/proc/CKey2ActiveTicket(ckey)
|
|
for(var/I in active_tickets)
|
|
var/datum/admin_help/AH = I
|
|
if(AH.initiator_ckey == ckey)
|
|
return AH
|
|
|
|
//
|
|
//TICKET LIST STATCLICK
|
|
//
|
|
|
|
/obj/effect/statclick/ticket_list
|
|
var/current_state
|
|
|
|
/obj/effect/statclick/ticket_list/Initialize(mapload, name, state)
|
|
. = ..()
|
|
current_state = state
|
|
|
|
/obj/effect/statclick/ticket_list/Click()
|
|
GLOB.ahelp_tickets.BrowseTickets(current_state)
|
|
|
|
//called by admin topic
|
|
/obj/effect/statclick/ticket_list/proc/Action()
|
|
Click()
|
|
|
|
/**
|
|
* # Adminhelp Ticket
|
|
*/
|
|
/datum/admin_help
|
|
/// Unique ID of the ticket
|
|
var/id
|
|
/// The current name of the ticket
|
|
var/name
|
|
/// The current state of the ticket
|
|
var/state = AHELP_ACTIVE
|
|
/// The time at which the ticket was opened
|
|
var/opened_at
|
|
/// The time at which the ticket was closed
|
|
var/closed_at
|
|
/// Semi-misnomer, it's the person who ahelped/was bwoinked
|
|
var/client/initiator
|
|
/// The ckey of the initiator
|
|
var/initiator_ckey
|
|
/// The key name of the initiator
|
|
var/initiator_key_name
|
|
/// If any admins were online when the ticket was initialized
|
|
var/heard_by_no_admins = FALSE
|
|
/// The collection of interactions with this ticket. Use AddInteraction() or, preferably, admin_ticket_log()
|
|
var/list/_interactions
|
|
/// Statclick holder for the ticket
|
|
var/obj/effect/statclick/ahelp/statclick
|
|
/// Static counter used for generating each ticket ID
|
|
var/static/ticket_counter = 0
|
|
|
|
/**
|
|
* Call this on its own to create a ticket, don't manually assign current_ticket
|
|
*
|
|
* Arguments:
|
|
* * msg_raw - The first message of this admin_help: used for the initial title of the ticket
|
|
* * is_bwoink - Boolean operator, TRUE if this ticket was started by an admin PM
|
|
*/
|
|
/datum/admin_help/New(msg_raw, client/C, is_bwoink, client/admin_C) //SKYRAT EDIT CHANGE
|
|
//clean the input msg
|
|
var/msg = sanitize(copytext_char(msg_raw, 1, MAX_MESSAGE_LEN))
|
|
if(!msg || !C || !C.mob)
|
|
qdel(src)
|
|
return
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN
|
|
if(admin_C && is_bwoink)
|
|
handler = "[admin_C.ckey]"
|
|
//SKYRAT EDIT END
|
|
|
|
id = ++ticket_counter
|
|
opened_at = world.time
|
|
|
|
name = copytext_char(msg, 1, 100)
|
|
|
|
initiator = C
|
|
initiator_ckey = initiator.ckey
|
|
initiator_key_name = key_name(initiator, FALSE, TRUE)
|
|
if(initiator.current_ticket) //This is a bug
|
|
stack_trace("Multiple ahelp current_tickets")
|
|
initiator.current_ticket.AddInteraction("Ticket erroneously left open by code")
|
|
initiator.current_ticket.Close()
|
|
initiator.current_ticket = src
|
|
|
|
TimeoutVerb()
|
|
|
|
statclick = new(null, src)
|
|
_interactions = list()
|
|
_interactions_player = list() // SKYRAT EDIT ADDITION -- Player ticket viewing
|
|
|
|
if(is_bwoink)
|
|
AddInteraction("<font color='blue'>[key_name_admin(usr)] PM'd [LinkedReplyName()]</font>")
|
|
AddInteractionPlayer("<font color='blue'>[key_name_admin(usr, FALSE)] PM'd [LinkedReplyName()]</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
|
|
message_admins("<font color='blue'>Ticket [TicketHref("#[id]")] created</font>")
|
|
else
|
|
MessageNoRecipient(msg_raw)
|
|
|
|
//send it to TGS if nobody is on and tell us how many were on
|
|
var/admin_number_present = send2tgs_adminless_only(initiator_ckey, "Ticket #[id]: [msg]")
|
|
log_admin_private("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.")
|
|
if(admin_number_present <= 0)
|
|
to_chat(C, span_notice("No active admins are online, your adminhelp was sent through TGS to admins who are available. This may use IRC or Discord."), confidential = TRUE)
|
|
heard_by_no_admins = TRUE
|
|
GLOB.ahelp_tickets.active_tickets += src
|
|
|
|
/datum/admin_help/Destroy()
|
|
RemoveActive()
|
|
GLOB.ahelp_tickets.closed_tickets -= src
|
|
GLOB.ahelp_tickets.resolved_tickets -= src
|
|
return ..()
|
|
|
|
/datum/admin_help/proc/AddInteraction(formatted_message)
|
|
if(heard_by_no_admins && usr && usr.ckey != initiator_ckey)
|
|
heard_by_no_admins = FALSE
|
|
send2adminchat(initiator_ckey, "Ticket #[id]: Answered by [key_name(usr)]")
|
|
_interactions += "[time_stamp()]: [formatted_message]"
|
|
|
|
//Removes the ahelp verb and returns it after 2 minutes
|
|
/datum/admin_help/proc/TimeoutVerb()
|
|
remove_verb(initiator, /client/verb/adminhelp)
|
|
initiator.adminhelptimerid = addtimer(CALLBACK(initiator, /client/proc/giveadminhelpverb), 1200, TIMER_STOPPABLE) //2 minute cooldown of admin helps
|
|
|
|
//private
|
|
/datum/admin_help/proc/FullMonty(ref_src)
|
|
if(!ref_src)
|
|
ref_src = "[REF(src)]"
|
|
. = ADMIN_FULLMONTY_NONAME(initiator.mob)
|
|
if(state == AHELP_ACTIVE)
|
|
if (CONFIG_GET(flag/popup_admin_pm))
|
|
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];adminpopup=[REF(initiator)]'>POPUP</A>)"
|
|
. += ClosureLinks(ref_src)
|
|
|
|
//private
|
|
/datum/admin_help/proc/ClosureLinks(ref_src)
|
|
if(!ref_src)
|
|
ref_src = "[REF(src)]"
|
|
. = " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=reject'>REJT</A>)"
|
|
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=icissue'>IC</A>)"
|
|
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=close'>CLOSE</A>)"
|
|
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=resolve'>RSLVE</A>)"
|
|
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=handleissue'>HANDLE</A>)" //SKYRAT EDIT ADDITION - ADMIN
|
|
|
|
//private
|
|
/datum/admin_help/proc/LinkedReplyName(ref_src)
|
|
if(!ref_src)
|
|
ref_src = "[REF(src)]"
|
|
return "<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=reply'>[initiator_key_name]</A>"
|
|
|
|
//private
|
|
/datum/admin_help/proc/TicketHref(msg, ref_src, action = "ticket")
|
|
if(!ref_src)
|
|
ref_src = "[REF(src)]"
|
|
return "<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=[action]'>[msg]</A>"
|
|
|
|
//message from the initiator without a target, all admins will see this
|
|
//won't bug irc/discord
|
|
/datum/admin_help/proc/MessageNoRecipient(msg)
|
|
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)]</span><b>: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]:</b> <span class='linkify'>[keywords_lookup(msg)]"))
|
|
|
|
AddInteraction("<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>")
|
|
AddInteractionPlayer("<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
|
|
log_admin_private("Ticket #[id]: [key_name(initiator)]: [msg]")
|
|
|
|
//send this msg to all admins
|
|
for(var/client/X in GLOB.admins)
|
|
if(X.prefs.toggles & SOUND_ADMINHELP)
|
|
SEND_SOUND(X, sound('sound/effects/adminhelp.ogg'))
|
|
window_flash(X, ignorepref = TRUE)
|
|
to_chat(X,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = admin_msg,
|
|
confidential = TRUE)
|
|
|
|
//show it to the person adminhelping too
|
|
to_chat(initiator,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_adminnotice("PM to-<b>Admins</b>: <span class='linkify'>[msg]</span>"),
|
|
confidential = TRUE)
|
|
SSblackbox.LogAhelp(id, "Ticket Opened", msg, null, initiator.ckey)
|
|
|
|
//Reopen a closed ticket
|
|
/datum/admin_help/proc/Reopen()
|
|
if(state == AHELP_ACTIVE)
|
|
to_chat(usr, span_warning("This ticket is already open."), confidential = TRUE)
|
|
return
|
|
|
|
if(GLOB.ahelp_tickets.CKey2ActiveTicket(initiator_ckey))
|
|
to_chat(usr, span_warning("This user already has an active ticket, cannot reopen this one."), confidential = TRUE)
|
|
return
|
|
|
|
statclick = new(null, src)
|
|
GLOB.ahelp_tickets.active_tickets += src
|
|
GLOB.ahelp_tickets.closed_tickets -= src
|
|
GLOB.ahelp_tickets.resolved_tickets -= src
|
|
switch(state)
|
|
if(AHELP_CLOSED)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", -1, "closed")
|
|
if(AHELP_RESOLVED)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", -1, "resolved")
|
|
state = AHELP_ACTIVE
|
|
closed_at = null
|
|
if(initiator)
|
|
initiator.current_ticket = src
|
|
|
|
AddInteraction("<font color='purple'>Reopened by [key_name_admin(usr)]</font>")
|
|
AddInteractionPlayer("<font color='purple'>Reopened by [key_name_admin(usr, FALSE)]</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
|
|
var/msg = span_adminhelp("Ticket [TicketHref("#[id]")] reopened by [key_name_admin(usr)].")
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
SSblackbox.LogAhelp(id, "Reopened", "Reopened by [usr.key]", usr.ckey)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "reopened")
|
|
TicketPanel() //can only be done from here, so refresh it
|
|
|
|
//private
|
|
/datum/admin_help/proc/RemoveActive()
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
closed_at = world.time
|
|
QDEL_NULL(statclick)
|
|
GLOB.ahelp_tickets.active_tickets -= src
|
|
if(initiator && initiator.current_ticket == src)
|
|
initiator.current_ticket = null
|
|
|
|
SEND_SIGNAL(src, COMSIG_ADMIN_HELP_MADE_INACTIVE)
|
|
|
|
//Mark open ticket as closed/meme
|
|
/datum/admin_help/proc/Close(key_name = key_name_admin(usr), silent = FALSE)
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
if(handler && handler != usr.ckey)
|
|
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
|
|
if(!response || response == "No")
|
|
return
|
|
//SKYRAT EDIT ADDITION END
|
|
RemoveActive()
|
|
state = AHELP_CLOSED
|
|
GLOB.ahelp_tickets.ListInsert(src)
|
|
AddInteraction("<font color='red'>Closed by [key_name].</font>")
|
|
AddInteractionPlayer("<font color='red'>Closed by [key_name_admin(usr, FALSE)].</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
|
|
if(!silent)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "closed")
|
|
var/msg = "Ticket [TicketHref("#[id]")] closed by [key_name]."
|
|
message_admins(msg)
|
|
SSblackbox.LogAhelp(id, "Closed", "Closed by [usr.key]", null, usr.ckey)
|
|
log_admin_private(msg)
|
|
|
|
//Mark open ticket as resolved/legitimate, returns ahelp verb
|
|
/datum/admin_help/proc/Resolve(key_name = key_name_admin(usr), silent = FALSE)
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
if(handler && handler != usr.ckey)
|
|
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
|
|
if(!response || response == "No")
|
|
return
|
|
//SKYRAT EDIT ADDITION END
|
|
|
|
RemoveActive()
|
|
state = AHELP_RESOLVED
|
|
GLOB.ahelp_tickets.ListInsert(src)
|
|
|
|
addtimer(CALLBACK(initiator, /client/proc/giveadminhelpverb), 50)
|
|
|
|
AddInteraction("<font color='green'>Resolved by [key_name].</font>")
|
|
AddInteractionPlayer("<font color='green'>Resolved by [key_name_admin(usr, FALSE)].</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
|
|
to_chat(initiator, span_adminhelp("Your ticket has been resolved by an admin. The Adminhelp verb will be returned to you shortly."), confidential = TRUE)
|
|
if(!silent)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "resolved")
|
|
var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name]"
|
|
message_admins(msg)
|
|
SSblackbox.LogAhelp(id, "Resolved", "Resolved by [usr.key]", null, usr.ckey)
|
|
log_admin_private(msg)
|
|
|
|
//Close and return ahelp verb, use if ticket is incoherent
|
|
/datum/admin_help/proc/Reject(key_name = key_name_admin(usr))
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
if(handler && handler != usr.ckey)
|
|
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
|
|
if(!response || response == "No")
|
|
return
|
|
//SKYRAT EDIT ADDITION END
|
|
|
|
if(initiator)
|
|
initiator.giveadminhelpverb()
|
|
|
|
SEND_SOUND(initiator, sound('sound/effects/adminhelp.ogg'))
|
|
|
|
to_chat(initiator, "<font color='red' size='4'><b>- AdminHelp Rejected! -</b></font>", confidential = TRUE)
|
|
to_chat(initiator, "<font color='red'><b>Your admin help was rejected.</b> The adminhelp verb has been returned to you so that you may try again.</font>", confidential = TRUE)
|
|
to_chat(initiator, "Please try to be calm, clear, and descriptive in admin helps, do not assume the admin has seen any related events, and clearly state the names of anybody you are reporting.", confidential = TRUE)
|
|
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "rejected")
|
|
var/msg = "Ticket [TicketHref("#[id]")] rejected by [key_name]"
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
AddInteraction("Rejected by [key_name].")
|
|
AddInteractionPlayer("Rejected by [key_name_admin(usr, FALSE)].") // SKYRAT EDIT ADDITION -- Player ticket viewing
|
|
SSblackbox.LogAhelp(id, "Rejected", "Rejected by [usr.key]", null, usr.ckey)
|
|
Close(silent = TRUE)
|
|
|
|
//Resolve ticket with IC Issue message
|
|
/datum/admin_help/proc/ICIssue(key_name = key_name_admin(usr))
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
if(handler && handler != usr.ckey)
|
|
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
|
|
if(!response || response == "No")
|
|
return
|
|
//SKYRAT EDIT ADDITION END
|
|
|
|
var/msg = "<font color='red' size='4'><b>- AdminHelp marked as IC issue! -</b></font><br>"
|
|
msg += "<font color='red'>Your issue has been determined by an administrator to be an in character issue and does NOT require administrator intervention at this time. For further resolution you should pursue options that are in character.</font>"
|
|
|
|
if(initiator)
|
|
to_chat(initiator, msg, confidential = TRUE)
|
|
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "IC")
|
|
msg = "Ticket [TicketHref("#[id]")] marked as IC by [key_name]"
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
AddInteraction("Marked as IC issue by [key_name]")
|
|
AddInteractionPlayer("Marked as IC issue by [key_name_admin(usr, FALSE)]") // SKYRAT EDIT ADDITION -- Player ticket viewing
|
|
SSblackbox.LogAhelp(id, "IC Issue", "Marked as IC issue by [usr.key]", null, usr.ckey)
|
|
Resolve(silent = TRUE)
|
|
|
|
//Show the ticket panel
|
|
/datum/admin_help/proc/TicketPanel()
|
|
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Ticket #[id]</title></head>")
|
|
var/ref_src = "[REF(src)]"
|
|
dat += "<h4>Admin Help Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
|
|
dat += "<b>State: [ticket_status()]</b>"
|
|
dat += "[FOURSPACES][TicketHref("Refresh", ref_src)][FOURSPACES][TicketHref("Re-Title", ref_src, "retitle")]"
|
|
if(state != AHELP_ACTIVE)
|
|
dat += "[FOURSPACES][TicketHref("Reopen", ref_src, "reopen")]"
|
|
dat += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [DisplayTimeText(world.time - opened_at)] ago)"
|
|
if(closed_at)
|
|
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [DisplayTimeText(world.time - closed_at)] ago)"
|
|
dat += "<br><br>"
|
|
if(initiator)
|
|
dat += "<b>Actions:</b> [FullMonty(ref_src)]<br>"
|
|
else
|
|
dat += "<b>DISCONNECTED</b>[FOURSPACES][ClosureLinks(ref_src)]<br>"
|
|
dat += "<br><b>Log:</b><br><br>"
|
|
for(var/I in _interactions)
|
|
dat += "[I]<br>"
|
|
|
|
// Append any tickets also opened by this user if relevant
|
|
var/list/related_tickets = GLOB.ahelp_tickets.TicketsByCKey(initiator_ckey)
|
|
if (related_tickets.len > 1)
|
|
dat += "<br/><b>Other Tickets by User</b><br/>"
|
|
for (var/datum/admin_help/related_ticket in related_tickets)
|
|
if (related_ticket.id == id)
|
|
continue
|
|
dat += "[related_ticket.TicketHref("#[related_ticket.id]")] ([related_ticket.ticket_status()]): [related_ticket.name]<br/>"
|
|
|
|
usr << browse(dat.Join(), "window=ahelp[id];size=700x480")
|
|
|
|
/**
|
|
* Renders the current status of the ticket into a displayable string
|
|
*/
|
|
/datum/admin_help/proc/ticket_status()
|
|
switch(state)
|
|
if(AHELP_ACTIVE)
|
|
return "<font color='red'>OPEN</font>"
|
|
if(AHELP_RESOLVED)
|
|
return "<font color='green'>RESOLVED</font>"
|
|
if(AHELP_CLOSED)
|
|
return "CLOSED"
|
|
else
|
|
stack_trace("Invalid ticket state: [state]")
|
|
return "INVALID, CALL A CODER"
|
|
|
|
/datum/admin_help/proc/Retitle()
|
|
var/new_title = input(usr, "Enter a title for the ticket", "Rename Ticket", name) as text|null
|
|
if(new_title)
|
|
name = new_title
|
|
//not saying the original name cause it could be a long ass message
|
|
var/msg = "Ticket [TicketHref("#[id]")] titled [name] by [key_name_admin(usr)]"
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
TicketPanel() //we have to be here to do this
|
|
|
|
//Forwarded action from admin/Topic
|
|
/datum/admin_help/proc/Action(action)
|
|
testing("Ahelp action: [action]")
|
|
switch(action)
|
|
if("ticket")
|
|
TicketPanel()
|
|
if("retitle")
|
|
Retitle()
|
|
if("reject")
|
|
Reject()
|
|
if("reply")
|
|
HandleIssue() /// SKYRAT EDIT ADDITION - ADMIN HANDLE
|
|
usr.client.cmd_ahelp_reply(initiator)
|
|
if("icissue")
|
|
ICIssue()
|
|
if("close")
|
|
Close()
|
|
if("resolve")
|
|
Resolve()
|
|
if("reopen")
|
|
Reopen()
|
|
//SKYRAT EDIT ADDITION BEING - ADMIN
|
|
if("handleissue")
|
|
HandleIssue()
|
|
//SKYRAT EDIT ADDITION END
|
|
|
|
//
|
|
// TICKET STATCLICK
|
|
//
|
|
|
|
/obj/effect/statclick/ahelp
|
|
var/datum/admin_help/ahelp_datum
|
|
|
|
/obj/effect/statclick/ahelp/Initialize(mapload, datum/admin_help/AH)
|
|
ahelp_datum = AH
|
|
. = ..()
|
|
|
|
/obj/effect/statclick/ahelp/update()
|
|
return ..(ahelp_datum.name)
|
|
|
|
/obj/effect/statclick/ahelp/Click()
|
|
ahelp_datum.TicketPanel()
|
|
|
|
/obj/effect/statclick/ahelp/Destroy()
|
|
ahelp_datum = null
|
|
return ..()
|
|
|
|
//
|
|
// CLIENT PROCS
|
|
//
|
|
|
|
/client/proc/giveadminhelpverb()
|
|
add_verb(src, /client/verb/adminhelp)
|
|
deltimer(adminhelptimerid)
|
|
adminhelptimerid = 0
|
|
|
|
// Used for methods where input via arg doesn't work
|
|
/client/proc/get_adminhelp()
|
|
var/msg = input(src, "Please describe your problem concisely and an admin will help as soon as they're able.", "Adminhelp contents") as message|null
|
|
adminhelp(msg)
|
|
|
|
/client/verb/adminhelp(msg as message)
|
|
set category = "Admin"
|
|
set name = "Adminhelp"
|
|
|
|
if(GLOB.say_disabled) //This is here to try to identify lag problems
|
|
to_chat(usr, span_danger("Speech is currently admin-disabled."), confidential = TRUE)
|
|
return
|
|
|
|
//handle muting and automuting
|
|
if(prefs.muted & MUTE_ADMINHELP)
|
|
to_chat(src, span_danger("Error: Admin-PM: You cannot send adminhelps (Muted)."), confidential = TRUE)
|
|
return
|
|
if(handle_spam_prevention(msg,MUTE_ADMINHELP))
|
|
return
|
|
|
|
msg = trim(msg)
|
|
|
|
if(!msg)
|
|
return
|
|
|
|
SSblackbox.record_feedback("tally", "admin_verb", 1, "Adminhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
|
if(current_ticket)
|
|
current_ticket.MessageNoRecipient(msg)
|
|
current_ticket.TimeoutVerb()
|
|
return
|
|
|
|
new /datum/admin_help(msg, src, FALSE)
|
|
|
|
|
|
//
|
|
// LOGGING
|
|
//
|
|
|
|
//Use this proc when an admin takes action that may be related to an open ticket on what
|
|
//what can be a client, ckey, or mob
|
|
// /proc/admin_ticket_log(what, message) // SKYRAT EDIT ORIGINAL
|
|
/proc/admin_ticket_log(what, message, admin_only = TRUE) // SKYRAT EDIT CHANGE -- Player ticket viewing
|
|
var/client/C
|
|
var/mob/Mob = what
|
|
if(istype(Mob))
|
|
C = Mob.client
|
|
else
|
|
C = what
|
|
if(istype(C) && C.current_ticket)
|
|
C.current_ticket.AddInteraction(message)
|
|
// SKYRAT EDIT ADDITION START -- Player ticket viewing
|
|
if(!admin_only)
|
|
C.current_ticket.AddInteractionPlayer(message)
|
|
// SKYRAT EDIT ADDITION END
|
|
return C.current_ticket
|
|
if(istext(what)) //ckey
|
|
var/datum/admin_help/AH = GLOB.ahelp_tickets.CKey2ActiveTicket(what)
|
|
if(AH)
|
|
AH.AddInteraction(message)
|
|
// SKYRAT EDIT ADDITION START -- Player ticket viewing
|
|
if(!admin_only)
|
|
AH.AddInteractionPlayer(message)
|
|
// SKYRAT EDIT ADDITION END
|
|
return AH
|
|
|
|
//
|
|
// HELPER PROCS
|
|
//
|
|
|
|
/proc/get_admin_counts(requiredflags = R_BAN)
|
|
. = list("total" = list(), "noflags" = list(), "afk" = list(), "stealth" = list(), "present" = list())
|
|
for(var/client/X in GLOB.admins)
|
|
.["total"] += X
|
|
if(requiredflags != NONE && !check_rights_for(X, requiredflags))
|
|
.["noflags"] += X
|
|
else if(X.is_afk())
|
|
.["afk"] += X
|
|
else if(X.holder.fakekey)
|
|
.["stealth"] += X
|
|
else
|
|
.["present"] += X
|
|
|
|
/proc/send2tgs_adminless_only(source, msg, requiredflags = R_BAN)
|
|
var/list/adm = get_admin_counts(requiredflags)
|
|
var/list/activemins = adm["present"]
|
|
. = activemins.len
|
|
if(. <= 0)
|
|
var/final = ""
|
|
var/list/afkmins = adm["afk"]
|
|
var/list/stealthmins = adm["stealth"]
|
|
var/list/powerlessmins = adm["noflags"]
|
|
var/list/allmins = adm["total"]
|
|
if(!afkmins.len && !stealthmins.len && !powerlessmins.len)
|
|
final = "[msg] - No admins online"
|
|
else
|
|
final = "[msg] - All admins stealthed\[[english_list(stealthmins)]\], AFK\[[english_list(afkmins)]\], or lacks +BAN\[[english_list(powerlessmins)]\]! Total: [allmins.len] "
|
|
send2adminchat(source,final)
|
|
send2otherserver(source,final)
|
|
|
|
/**
|
|
* Sends a message to a set of cross-communications-enabled servers using world topic calls
|
|
*
|
|
* Arguments:
|
|
* * source - Who sent this message
|
|
* * msg - The message body
|
|
* * type - The type of message, becomes the topic command under the hood
|
|
* * target_servers - A collection of servers to send the message to, defined in config
|
|
* * additional_data - An (optional) associated list of extra parameters and data to send with this world topic call
|
|
*/
|
|
/proc/send2otherserver(source, msg, type = "Ahelp", target_servers, list/additional_data = list())
|
|
if(!CONFIG_GET(string/comms_key))
|
|
debug_world_log("Server cross-comms message not sent for lack of configured key")
|
|
return
|
|
|
|
var/our_id = CONFIG_GET(string/cross_comms_name)
|
|
additional_data["message_sender"] = source
|
|
additional_data["message"] = msg
|
|
additional_data["source"] = "([our_id])"
|
|
additional_data += type
|
|
|
|
var/list/servers = CONFIG_GET(keyed_list/cross_server)
|
|
for(var/I in servers)
|
|
if(I == our_id) //No sending to ourselves
|
|
continue
|
|
if(target_servers && !(I in target_servers))
|
|
continue
|
|
world.send_cross_comms(I, additional_data)
|
|
|
|
/// Sends a message to a given cross comms server by name (by name for security).
|
|
/world/proc/send_cross_comms(server_name, list/message, auth = TRUE)
|
|
set waitfor = FALSE
|
|
if (auth)
|
|
var/comms_key = CONFIG_GET(string/comms_key)
|
|
if(!comms_key)
|
|
debug_world_log("Server cross-comms message not sent for lack of configured key")
|
|
return
|
|
message["key"] = comms_key
|
|
var/list/servers = CONFIG_GET(keyed_list/cross_server)
|
|
var/server_url = servers[server_name]
|
|
if (!server_url)
|
|
CRASH("Invalid cross comms config: [server_name]")
|
|
world.Export("[server_url]?[list2params(message)]")
|
|
|
|
|
|
/proc/tgsadminwho()
|
|
var/list/message = list("Admins: ")
|
|
var/list/admin_keys = list()
|
|
for(var/adm in GLOB.admins)
|
|
var/client/C = adm
|
|
admin_keys += "[C][C.holder.fakekey ? "(Stealth)" : ""][C.is_afk() ? "(AFK)" : ""]"
|
|
|
|
for(var/admin in admin_keys)
|
|
if(LAZYLEN(message) > 1)
|
|
message += ", [admin]"
|
|
else
|
|
message += "[admin]"
|
|
|
|
return jointext(message, "")
|
|
|
|
/proc/keywords_lookup(msg,external)
|
|
|
|
//This is a list of words which are ignored by the parser when comparing message contents for names. MUST BE IN LOWER CASE!
|
|
var/list/adminhelp_ignored_words = list("unknown","the","a","an","of","monkey","alien","as", "i")
|
|
|
|
//explode the input msg into a list
|
|
var/list/msglist = splittext(msg, " ")
|
|
|
|
//generate keywords lookup
|
|
var/list/surnames = list()
|
|
var/list/forenames = list()
|
|
var/list/ckeys = list()
|
|
var/founds = ""
|
|
for(var/mob/M in GLOB.mob_list)
|
|
var/list/indexing = list(M.real_name, M.name)
|
|
if(M.mind)
|
|
indexing += M.mind.name
|
|
|
|
for(var/string in indexing)
|
|
var/list/L = splittext(string, " ")
|
|
var/surname_found = 0
|
|
//surnames
|
|
for(var/i=L.len, i>=1, i--)
|
|
var/word = ckey(L[i])
|
|
if(word)
|
|
surnames[word] = M
|
|
surname_found = i
|
|
break
|
|
//forenames
|
|
for(var/i=1, i<surname_found, i++)
|
|
var/word = ckey(L[i])
|
|
if(word)
|
|
forenames[word] = M
|
|
//ckeys
|
|
ckeys[M.ckey] = M
|
|
|
|
var/ai_found = 0
|
|
msg = ""
|
|
var/list/mobs_found = list()
|
|
for(var/original_word in msglist)
|
|
var/word = ckey(original_word)
|
|
if(word)
|
|
if(!(word in adminhelp_ignored_words))
|
|
if(word == "ai")
|
|
ai_found = 1
|
|
else
|
|
var/mob/found = ckeys[word]
|
|
if(!found)
|
|
found = surnames[word]
|
|
if(!found)
|
|
found = forenames[word]
|
|
if(found)
|
|
if(!(found in mobs_found))
|
|
mobs_found += found
|
|
if(!ai_found && isAI(found))
|
|
ai_found = 1
|
|
var/is_antag = 0
|
|
if(is_special_character(found))
|
|
is_antag = 1
|
|
founds += "Name: [found.name]([found.real_name]) Key: [found.key] Ckey: [found.ckey] [is_antag ? "(Antag)" : null] "
|
|
msg += "[original_word]<font size='1' color='[is_antag ? "red" : "black"]'>(<A HREF='?_src_=holder;[HrefToken(TRUE)];adminmoreinfo=[REF(found)]'>?</A>|<A HREF='?_src_=holder;[HrefToken(TRUE)];adminplayerobservefollow=[REF(found)]'>F</A>)</font> "
|
|
continue
|
|
msg += "[original_word] "
|
|
if(external)
|
|
if(founds == "")
|
|
return "Search Failed"
|
|
else
|
|
return founds
|
|
|
|
return msg
|
|
|
|
/proc/get_mob_by_name(msg)
|
|
//This is a list of words which are ignored by the parser when comparing message contents for names. MUST BE IN LOWER CASE!
|
|
var/list/ignored_words = list("unknown","the","a","an","of","monkey","alien","as", "i")
|
|
|
|
//explode the input msg into a list
|
|
var/list/msglist = splittext(msg, " ")
|
|
|
|
//who might fit the shoe
|
|
var/list/potential_hits = list()
|
|
|
|
for(var/i in GLOB.mob_list)
|
|
var/mob/M = i
|
|
var/list/nameWords = list()
|
|
if(!M.mind)
|
|
continue
|
|
|
|
for(var/string in splittext(lowertext(M.real_name), " "))
|
|
if(!(string in ignored_words))
|
|
nameWords += string
|
|
for(var/string in splittext(lowertext(M.name), " "))
|
|
if(!(string in ignored_words))
|
|
nameWords += string
|
|
|
|
for(var/string in nameWords)
|
|
if(string in msglist)
|
|
potential_hits += M
|
|
break
|
|
|
|
return potential_hits
|
|
|
|
/**
|
|
* Checks a given message to see if any of the words contain an active admin's ckey with an @ before it
|
|
*
|
|
* Returns nothing if no pings are found, otherwise returns an associative list with ckey -> client
|
|
* Also modifies msg to underline the pings, then stores them in the key [ADMINSAY_PING_UNDERLINE_NAME_INDEX] for returning
|
|
*
|
|
* Arguments:
|
|
* * msg - the message being scanned
|
|
*/
|
|
/proc/check_admin_pings(msg)
|
|
//explode the input msg into a list
|
|
var/list/msglist = splittext(msg, " ")
|
|
var/list/admins_to_ping = list()
|
|
|
|
var/i = 0
|
|
for(var/word in msglist)
|
|
i++
|
|
if(word[1] != "@")
|
|
continue
|
|
var/ckey_check = lowertext(copytext(word, 2))
|
|
var/client/client_check = GLOB.directory[ckey_check]
|
|
if(client_check?.holder)
|
|
msglist[i] = "<u>[word]</u>"
|
|
admins_to_ping[ckey_check] = client_check
|
|
|
|
if(length(admins_to_ping))
|
|
admins_to_ping[ADMINSAY_PING_UNDERLINE_NAME_INDEX] = jointext(msglist, " ") // without tuples, we must make do!
|
|
return admins_to_ping
|