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

" for(var/I in l2b) var/datum/admin_help/AH = I dat += "[span_adminnotice("[span_adminhelp("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() 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)) 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)) 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.") //Gotta async this cause clients only logout on destroy, and sleeping in destroy is disgusting INVOKE_ASYNC(SSblackbox, /datum/controller/subsystem/blackbox/proc/LogAhelp, T.id, "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() if (!usr.client?.holder) message_admins("[key_name_admin(usr)] non-holder clicked on a ticket list statclick! ([src])") log_game("[key_name(usr)] non-holder clicked on a ticket list statclick! ([src])") return 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/ticket_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 /// The list of clients currently responding to the opening ticket before it gets a response var/list/opening_responders /** * 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, urgent = FALSE) //clean the input msg var/msg = sanitize(copytext_char(msg_raw, 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) ticket_interactions = list() if(is_bwoink) AddInteraction("[key_name_admin(usr)] PM'd [LinkedReplyName()]") message_admins("Ticket [TicketHref("#[id]")] created") else MessageNoRecipient(msg_raw, urgent) send_message_to_tgs(msg, urgent) GLOB.ahelp_tickets.active_tickets += src /datum/admin_help/proc/send_message_to_tgs(message, urgent = FALSE) var/message_to_send = message if(urgent) var/extra_message = CONFIG_GET(string/urgent_ahelp_message) to_chat(initiator, span_boldwarning("Notified admins to prioritize your ticket")) var/datum/discord_embed/embed = new() embed.title = "Ticket #[id]" embed.author = key_name(initiator_ckey) var/round_state switch(SSticker.current_state) if(GAME_STATE_STARTUP, GAME_STATE_PREGAME, GAME_STATE_SETTING_UP) round_state = "Round has not started" if(GAME_STATE_PLAYING) round_state = "Round is ongoing." if(SSshuttle.emergency.getModeStr()) round_state += "\n[SSshuttle.emergency.getModeStr()]: [SSshuttle.emergency.getTimerStr()]" if(SSticker.emergency_reason) round_state += ", Shuttle call reason: [SSticker.emergency_reason]" if(GAME_STATE_FINISHED) round_state = "Round has ended" var/list/admin_counts = get_admin_counts(R_BAN) var/stealth_admins = jointext(admin_counts["stealth"], ", ") var/afk_admins = jointext(admin_counts["afk"], ", ") var/other_admins = jointext(admin_counts["noflags"], ", ") var/admin_text = "" if(stealth_admins) admin_text += "**Stealthed**: [stealth_admins]\n" if(afk_admins) admin_text += "**AFK**: [afk_admins]\n" if(other_admins) admin_text += "**Lacks +BAN**: [other_admins]\n" embed.fields = list( "CKEY" = initiator_ckey, "ROUND STATE" = round_state, "ROUND ID" = GLOB.round_id, "ROUND TIME" = ROUND_TIME, "MESSAGE" = message, "ADMINS" = admin_text, ) embed.content = extra_message if(CONFIG_GET(string/adminhelp_ahelp_link)) var/ahelp_link = replacetext(CONFIG_GET(string/adminhelp_ahelp_link), "$RID", GLOB.round_id) ahelp_link = replacetext(ahelp_link, "$TID", id) embed.url = ahelp_link embed.footer = "This player requested an admin" send2adminchat_webhook(embed) //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]: [message_to_send]") 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(initiator, span_notice("No active admins are online, your adminhelp was sent to admins who are available through IRC or Discord."), confidential = TRUE) heard_by_no_admins = TRUE /proc/send2adminchat_webhook(message_or_embed) if(!CONFIG_GET(string/adminhelp_webhook_url)) return var/list/webhook_info = list() if(istext(message_or_embed)) var/message_content = replacetext(replacetext(message_or_embed, "\proper", ""), "\improper", "") message_content = GLOB.has_discord_embeddable_links.Replace(replacetext(message_content, "`", ""), " ```$1``` ") webhook_info["content"] = message_content else var/datum/discord_embed/embed = message_or_embed webhook_info["embeds"] = list(embed.convert_to_list()) if(embed.content) webhook_info["content"] = embed.content if(CONFIG_GET(string/adminhelp_webhook_name)) webhook_info["username"] = CONFIG_GET(string/adminhelp_webhook_name) if(CONFIG_GET(string/adminhelp_webhook_pfp)) webhook_info["avatar_url"] = CONFIG_GET(string/adminhelp_webhook_pfp) // Uncomment when servers are moved to TGS4 // send2chat("[initiator_ckey] | [message_content]", "ahelp", TRUE) var/list/headers = list() headers["Content-Type"] = "application/json" var/datum/http_request/request = new() request.prepare(RUSTG_HTTP_METHOD_POST, CONFIG_GET(string/adminhelp_webhook_url), json_encode(webhook_info), headers, "tmp/response.json") request.begin_async() /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)]") ticket_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)) . += " (POPUP)" . += 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, urgent = FALSE) msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN)) var/ref_src = "[REF(src)]" //Message to be sent to all admins var/admin_msg = span_adminnotice(span_adminhelp("Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]: [keywords_lookup(msg)]")) 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, 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-Admins: [msg]"), confidential = TRUE) SSblackbox.LogAhelp(id, "Ticket Opened", msg, null, initiator.ckey, urgent = urgent) //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("Reopened by [key_name_admin(usr)]") 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 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, 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 if(initiator) initiator.giveadminhelpverb() SEND_SOUND(initiator, sound('sound/effects/adminhelp.ogg')) to_chat(initiator, "- AdminHelp Rejected! -", confidential = TRUE) to_chat(initiator, "Your admin help was rejected. The adminhelp verb has been returned to you so that you may try again.", 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 = "- 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, 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("Ticket #[id]") var/ref_src = "[REF(src)]" dat += "

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

" dat += "State: [ticket_status()]" 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 ticket_interactions) dat += "[I]
" // 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 += "
Other Tickets by User
" 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]
" 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 "OPEN" if(AHELP_RESOLVED) return "RESOLVED" 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") 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() if (!usr.client?.holder) message_admins("[key_name_admin(usr)] non-holder clicked on an ahelp statclick! ([src])") log_game("[key_name(usr)] non-holder clicked on an ahelp statclick! ([src])") return 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 GLOBAL_DATUM_INIT(admin_help_ui_handler, /datum/admin_help_ui_handler, new) /datum/admin_help_ui_handler var/list/ahelp_cooldowns = list() /datum/admin_help_ui_handler/ui_state(mob/user) return GLOB.always_state /datum/admin_help_ui_handler/ui_data(mob/user) . = list() var/list/admins = get_admin_counts(R_BAN) .["adminCount"] = length(admins["present"]) /datum/admin_help_ui_handler/ui_static_data(mob/user) . = list() .["bannedFromUrgentAhelp"] = is_banned_from(user.ckey, "Urgent Adminhelp") .["urgentAhelpPromptMessage"] = CONFIG_GET(string/urgent_ahelp_user_prompt) var/webhook_url = CONFIG_GET(string/adminhelp_webhook_url) if(webhook_url) .["urgentAhelpEnabled"] = TRUE /datum/admin_help_ui_handler/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, "Adminhelp") ui.open() ui.set_autoupdate(FALSE) /datum/admin_help_ui_handler/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) . = ..() if(.) return var/client/user_client = usr.client var/message = sanitize_text(trim(params["message"])) var/urgent = !!params["urgent"] var/list/admins = get_admin_counts(R_BAN) if(length(admins["present"]) != 0 || is_banned_from(user_client.ckey, "Urgent Adminhelp")) urgent = FALSE if(user_client.adminhelptimerid) return perform_adminhelp(user_client, message, urgent) ui.close() /datum/admin_help_ui_handler/proc/perform_adminhelp(client/user_client, message, urgent) 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 if(!message) return //handle muting and automuting if(user_client.prefs.muted & MUTE_ADMINHELP) to_chat(user_client, span_danger("Error: Admin-PM: You cannot send adminhelps (Muted)."), confidential = TRUE) return if(user_client.handle_spam_prevention(message, MUTE_ADMINHELP)) 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(urgent) if(!COOLDOWN_FINISHED(src, ahelp_cooldowns?[user_client.ckey])) urgent = FALSE // Prevent abuse else COOLDOWN_START(src, ahelp_cooldowns[user_client.ckey], CONFIG_GET(number/urgent_ahelp_cooldown) * (1 SECONDS)) if(user_client.current_ticket) user_client.current_ticket.TimeoutVerb() if(urgent) var/sanitized_message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN)) user_client.current_ticket.send_message_to_tgs(sanitized_message, urgent = TRUE) user_client.current_ticket.MessageNoRecipient(message, urgent) return new /datum/admin_help(message, user_client, FALSE, urgent) /client/verb/no_tgui_adminhelp(message as message) set name = "NoTguiAdminhelp" set hidden = TRUE if(adminhelptimerid) return message = trim(message) GLOB.admin_help_ui_handler.perform_adminhelp(src, message, FALSE) /client/verb/adminhelp() set category = "Admin" set name = "Adminhelp" GLOB.admin_help_ui_handler.ui_interact(mob) to_chat(src, span_boldnotice("Adminhelp failing to open or work? Click here")) // // 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 //log_in_blackbox: Whether or not this message with the blackbox system. //If disabled, this message should be logged with a different proc call /proc/admin_ticket_log(what, message, log_in_blackbox = TRUE) 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) if(log_in_blackbox) SSblackbox.LogAhelp(C.current_ticket.id, "Interaction", message, C.ckey, usr.ckey) return C.current_ticket if(istext(what)) //ckey var/datum/admin_help/AH = GLOB.ahelp_tickets.CKey2ActiveTicket(what) if(AH) AH.AddInteraction(message) if(log_in_blackbox) SSblackbox.LogAhelp(AH.id, "Interaction", message, what, usr.ckey) 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 in 1 to surname_found-1) 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](?|F) " 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(!length(word)) continue 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] = "[word]" 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 /** * Checks a given message to see if any of the words contain a memory ref for a datum. Said ref should not have brackets around it * * Returns nothing if no refs are found, otherwise returns an associative list with ckey -> client * Also modifies msg to underline and linkify the [ref] so other admins can click on the address to open the VV entry for said datum * * Arguments: * * msg - the message being scanned */ /proc/check_memory_refs(msg) if(!findtext(msg, GLOB.is_memref)) return //explode the input msg into a list var/list/msglist = splittext(msg, " ") var/list/datums_to_ref = list() var/i = 0 for(var/word in msglist) i++ if(!length(word)) continue var/word_with_brackets = "\[[word]\]" // the actual memory address lookups need the bracket wraps var/datum/check_datum = locate(word_with_brackets) if(!istype(check_datum)) continue msglist[i] = "[word_with_brackets]" datums_to_ref[word] = word if(length(datums_to_ref)) datums_to_ref[ADMINSAY_LINK_DATUM_REF] = jointext(msglist, " ") // without tuples, we must make do! return datums_to_ref