/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("[title]") dat += "Refresh

" for(var/I in l2b) var/datum/admin_help/AH = I dat += "Ticket #[AH.id]: [AH.initiator_key_name]: [AH.name]
" 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) C.current_ticket.AddInteraction("Client disconnected.") SSblackbox.LogAhelp(C.current_ticket.id, "Disconnected", "Client disconnected", C.ckey) C.current_ticket.initiator = null C.current_ticket = 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 = msg 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("[key_name_admin(usr)] PM'd [LinkedReplyName()]") message_admins("Ticket [TicketHref("#[id]")] created") 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]: [name]") 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, "No active admins are online, your adminhelp was sent through TGS to admins who are available. This may use IRC or Discord.") 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 send2tgs(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)]" . = " (REJT)" . += " (IC)" . += " (CLOSE)" . += " (RSLVE)" //private /datum/admin_help/proc/LinkedReplyName(ref_src) if(!ref_src) ref_src = "[REF(src)]" return "[initiator_key_name]" //private /datum/admin_help/proc/TicketHref(msg, ref_src, action = "ticket") if(!ref_src) ref_src = "[REF(src)]" return "[msg]" //message from the initiator without a target, all admins will see this //won't bug irc/discord /datum/admin_help/proc/MessageNoRecipient(msg) var/ref_src = "[REF(src)]" //Message to be sent to all admins var/admin_msg = "Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]: [keywords_lookup(msg)]" AddInteraction("[LinkedReplyName(ref_src)]: [msg]") 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, admin_msg) //show it to the person adminhelping too to_chat(initiator, "PM to-Admins: [msg]") 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, "This ticket is already open.") return if(GLOB.ahelp_tickets.CKey2ActiveTicket(initiator_ckey)) to_chat(usr, "This user already has an active ticket, cannot reopen this one.") 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("Reopened by [key_name_admin(usr)]") var/msg = "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 //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("Closed by [key_name].") 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("Resolved by [key_name].") to_chat(initiator, "Your ticket has been resolved by an admin. The Adminhelp verb will be returned to you shortly.") 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, "- AdminHelp Rejected! -") to_chat(initiator, "Your admin help was rejected. The adminhelp verb has been returned to you so that you may try again.") 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.") 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 = "- AdminHelp marked as IC issue! -
" msg += "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." if(initiator) to_chat(initiator, msg) 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("Ticket #[id]") var/ref_src = "[REF(src)]" dat += "

Admin Help Ticket #[id]: [LinkedReplyName(ref_src)]

" dat += "State: " switch(state) if(AHELP_ACTIVE) dat += "OPEN" if(AHELP_RESOLVED) dat += "RESOLVED" if(AHELP_CLOSED) dat += "CLOSED" else dat += "UNKNOWN" 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 += "

Opened at: [gameTimestamp(wtime = opened_at)] (Approx [DisplayTimeText(world.time - opened_at)] ago)" if(closed_at) dat += "
Closed at: [gameTimestamp(wtime = closed_at)] (Approx [DisplayTimeText(world.time - closed_at)] ago)" dat += "

" if(initiator) dat += "Actions: [FullMonty(ref_src)]
" else dat += "DISCONNECTED[FOURSPACES][ClosureLinks(ref_src)]
" dat += "
Log:

" for(var/I in _interactions) dat += "[I]
" 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 text|null adminhelp(msg) /client/verb/adminhelp(msg as text) set category = "Admin" set name = "Adminhelp" if(GLOB.say_disabled) //This is here to try to identify lag problems to_chat(usr, "Speech is currently admin-disabled.") return //handle muting and automuting if(prefs.muted & MUTE_ADMINHELP) to_chat(src, "Error: Admin-PM: You cannot send adminhelps (Muted).") 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) if(alert(usr, "You already have a ticket open. Is this for the same issue?",,"Yes","No") != "No") if(current_ticket) current_ticket.MessageNoRecipient(msg) current_ticket.TimeoutVerb() return else to_chat(usr, "Ticket not found, creating new one...") else current_ticket.AddInteraction("[key_name_admin(usr)] opened a new ticket.") current_ticket.Close() 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] " send2tgs(source,final) send2otherserver(source,final) /proc/send2tgs(msg,msg2) msg = replacetext(replacetext(msg, "\proper", ""), "\improper", "") msg2 = replacetext(replacetext(msg2, "\proper", ""), "\improper", "") world.TgsTargetedChatBroadcast("[msg] | [msg2]", TRUE) /proc/send2otherserver(source,msg,type = "Ahelp") var/comms_key = CONFIG_GET(string/comms_key) if(!comms_key) return var/list/message = list() message["message_sender"] = source message["message"] = msg message["source"] = "([CONFIG_GET(string/cross_comms_name)])" message["key"] = comms_key message += type var/list/servers = CONFIG_GET(keyed_list/cross_server) for(var/I in servers) world.Export("[servers[I]]?[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(?|F) " continue msg += "[original_word] " if(external) if(founds == "") return "Search Failed" else return founds return msg