mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-16 03:56:20 +00:00
This PR changes the message pipeline a little bit to support list-based messages, which can be annotated with custom data. Function signature of to_chat was slightly changed as well:
// Plain text message
to_chat(client,
type = MESSAGE_TYPE_INFO,
text = "foo")
// HTML message
to_chat(client,
type = MESSAGE_TYPE_INFO,
html = "<span class='notice'>foo</span>")
Old to_chat format is still supported, but handle_whitespace, trailing_newline and confidential flags have no effect. confidential flag could still be revived though, if there is enough merit in it, for example to filter out confidential messages when saving a chat log.
The reason for using /list and not /datum, is because lists are plain faster, and there are minimal data transformations - these lists are fed directly to json_encode and sent to tgchat.
Plain text messages do not need to be HTML-escaped, which makes them safer and more performant than HTML messages. Plain text messages can be made interactive (or formatted with CSS) by custom-handling them in javscript based on message type and annotations.
It would be impossible to annotate every single message in the game (at the moment of writing, there are 9447 to_chat calls in the code), but it could be done selectively, for only those messages that are hard to classify by span classes (and there are still A LOT of them).
Please annotate more messages. Thank you.
Fixes #52943
Fixes #52908
Fixes #52816
Changelog
cl
add: tgchat: Unread message count is now smarter and won't increase on other tabs if you have already read the same message in the active tab.
add: tgchat: Admin PMs are now properly annotated and can be filtered into separate tabs.
fix: tgchat: Fix: Highlighted message overlay no longer blocks clicks. Clicking a highlighted (F) link should work as it should.
fix: tgui: Fixed NTOS bluescreen due to calling .includes() on a stylesheet href which could be null on certain browsers.
code: tgchat: Chat schema bumped to version 5. All chat-related settings were reset to avoid breakage.
/cl
735 lines
24 KiB
Plaintext
735 lines
24 KiB
Plaintext
/client/var/adminhelptimerid = 0 //a timer id for returning the ahelp verb
|
|
/client/var/datum/admin_help/current_ticket //the current ticket the (usually) not-admin client is dealing with
|
|
|
|
//
|
|
//TICKET MANAGER
|
|
//
|
|
|
|
GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
|
|
|
/datum/admin_help_tickets
|
|
var/list/active_tickets = list()
|
|
var/list/closed_tickets = list()
|
|
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 class='adminnotice'><span class='adminhelp'>Ticket #[AH.id]</span>: <A href='?_src_=holder;[HrefToken()];ahelp=[REF(AH)];ahelp_action=ticket'>[AH.initiator_key_name]: [AH.name]</A></span><br>"
|
|
|
|
usr << browse(dat.Join(), "window=ahelp_list[state];size=600x480")
|
|
|
|
//Tickets statpanel
|
|
/datum/admin_help_tickets/proc/stat_entry()
|
|
var/num_disconnected = 0
|
|
stat("Active Tickets:", astatclick.update("[active_tickets.len]"))
|
|
for(var/I in active_tickets)
|
|
var/datum/admin_help/AH = I
|
|
if(AH.initiator)
|
|
stat("#[AH.id]. [AH.initiator_key_name]:", AH.statclick.update())
|
|
else
|
|
++num_disconnected
|
|
if(num_disconnected)
|
|
stat("Disconnected:", astatclick.update("[num_disconnected]"))
|
|
stat("Closed Tickets:", cstatclick.update("[closed_tickets.len]"))
|
|
stat("Resolved Tickets:", rstatclick.update("[resolved_tickets.len]"))
|
|
|
|
//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/New(loc, name, state)
|
|
current_state = state
|
|
..()
|
|
|
|
/obj/effect/statclick/ticket_list/Click()
|
|
GLOB.ahelp_tickets.BrowseTickets(current_state)
|
|
|
|
//
|
|
//TICKET DATUM
|
|
//
|
|
|
|
/datum/admin_help
|
|
var/id
|
|
var/name
|
|
var/state = AHELP_ACTIVE
|
|
|
|
var/opened_at
|
|
var/closed_at
|
|
|
|
var/client/initiator //semi-misnomer, it's the person who ahelped/was bwoinked
|
|
var/initiator_ckey
|
|
var/initiator_key_name
|
|
var/heard_by_no_admins = FALSE
|
|
|
|
var/list/_interactions //use AddInteraction() or, preferably, admin_ticket_log()
|
|
|
|
var/obj/effect/statclick/ahelp/statclick
|
|
|
|
var/static/ticket_counter = 0
|
|
|
|
//call this on its own to create a ticket, don't manually assign current_ticket
|
|
//msg is the title of the ticket: usually the ahelp text
|
|
//is_bwoink is TRUE if this ticket was started by an admin PM
|
|
/datum/admin_help/New(msg, client/C, is_bwoink)
|
|
//clean the input msg
|
|
msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN))
|
|
if(!msg || !C || !C.mob)
|
|
qdel(src)
|
|
return
|
|
|
|
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()
|
|
|
|
if(is_bwoink)
|
|
AddInteraction("<font color='blue'>[key_name_admin(usr)] PM'd [LinkedReplyName()]</font>")
|
|
message_admins("<font color='blue'>Ticket [TicketHref("#[id]")] created</font>")
|
|
else
|
|
MessageNoRecipient(msg)
|
|
|
|
//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 class='notice'>No active admins are online, your adminhelp was sent through TGS to admins who are available. This may use IRC or Discord.</span>", 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()
|
|
initiator.verbs -= /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)
|
|
. += 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>)"
|
|
|
|
//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 class='adminnotice'><span class='adminhelp'>Ticket [TicketHref("#[id]", ref_src)]</span><b>: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]:</b> <span class='linkify'>[keywords_lookup(msg)]</span></span>"
|
|
|
|
AddInteraction("<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>")
|
|
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 class='adminnotice'>PM to-<b>Admins</b>: <span class='linkify'>[msg]</span></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 class='warning'>This ticket is already open.</span>", confidential = TRUE)
|
|
return
|
|
|
|
if(GLOB.ahelp_tickets.CKey2ActiveTicket(initiator_ckey))
|
|
to_chat(usr, "<span class='warning'>This user already has an active ticket, cannot reopen this one.</span>", 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>")
|
|
var/msg = "<span class='adminhelp'>Ticket [TicketHref("#[id]")] reopened by [key_name_admin(usr)].</span>"
|
|
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
|
|
|
|
//Mark open ticket as closed/meme
|
|
/datum/admin_help/proc/Close(key_name = key_name_admin(usr), silent = FALSE)
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
RemoveActive()
|
|
state = AHELP_CLOSED
|
|
GLOB.ahelp_tickets.ListInsert(src)
|
|
AddInteraction("<font color='red'>Closed by [key_name].</font>")
|
|
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
|
|
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>")
|
|
to_chat(initiator, "<span class='adminhelp'>Your ticket has been resolved by an admin. The Adminhelp verb will be returned to you shortly.</span>", 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
|
|
|
|
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].")
|
|
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
|
|
|
|
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]")
|
|
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: "
|
|
switch(state)
|
|
if(AHELP_ACTIVE)
|
|
dat += "<font color='red'>OPEN</font>"
|
|
if(AHELP_RESOLVED)
|
|
dat += "<font color='green'>RESOLVED</font>"
|
|
if(AHELP_CLOSED)
|
|
dat += "CLOSED"
|
|
else
|
|
dat += "UNKNOWN"
|
|
dat += "</b>[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>"
|
|
|
|
usr << browse(dat.Join(), "window=ahelp[id];size=620x480")
|
|
|
|
/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")
|
|
usr.client.cmd_ahelp_reply(initiator)
|
|
if("icissue")
|
|
ICIssue()
|
|
if("close")
|
|
Close()
|
|
if("resolve")
|
|
Resolve()
|
|
if("reopen")
|
|
Reopen()
|
|
|
|
//
|
|
// 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()
|
|
src.verbs |= /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 class='danger'>Speech is currently admin-disabled.</span>", confidential = TRUE)
|
|
return
|
|
|
|
//handle muting and automuting
|
|
if(prefs.muted & MUTE_ADMINHELP)
|
|
to_chat(src, "<span class='danger'>Error: Admin-PM: You cannot send adminhelps (Muted).</span>", 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)
|
|
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)
|
|
return C.current_ticket
|
|
if(istext(what)) //ckey
|
|
var/datum/admin_help/AH = GLOB.ahelp_tickets.CKey2ActiveTicket(what)
|
|
if(AH)
|
|
AH.AddInteraction(message)
|
|
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 != 0 && !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 other servers.
|
|
/proc/send2otherserver(source,msg,type = "Ahelp",target_servers)
|
|
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)
|
|
var/list/message = list()
|
|
message["message_sender"] = source
|
|
message["message"] = msg
|
|
message["source"] = "([our_id])"
|
|
message += 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, message)
|
|
|
|
/// 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)
|
|
testing("Name word [string]")
|
|
if(string in msglist)
|
|
potential_hits += M
|
|
break
|
|
|
|
return potential_hits
|