//Defines //Deciseconds until ticket becomes stale if unanswered. Alerts admins. #define TICKET_TIMEOUT 6000 // 10 minutes //Decisecions before the user is allowed to open another ticket while their existing one is open. #define TICKET_DUPLICATE_COOLDOWN 3000 // 5 minutes //Status defines #define TICKET_OPEN 1 #define TICKET_CLOSED 2 #define TICKET_RESOLVED 3 #define TICKET_STALE 4 SUBSYSTEM_DEF(tickets) name = "Admin Tickets" init_order = INIT_ORDER_TICKETS wait = 300 priority = FIRE_PRIORITY_TICKETS offline_implications = "Admin tickets will no longer be marked as stale. No immediate action is needed." flags = SS_BACKGROUND var/span_class = "adminticket" var/ticket_system_name = "Admin Tickets" var/ticket_name = "Admin Ticket" var/close_rights = R_ADMIN var/rights_needed = R_ADMIN | R_MOD /// The name of the other ticket type to convert to var/other_ticket_name = "Mentor" /// Which permission to look for when seeing if there is staff available for the other ticket type var/other_ticket_permission = R_MENTOR var/list/close_messages var/list/allTickets = list() //make it here because someone might ahelp before the system has initialized var/ticketCounter = 1 /datum/controller/subsystem/tickets/Initialize() close_messages = list("- [ticket_name] Rejected! -", "Please try to be calm, clear, and descriptive in admin helps, do not assume the staff member has seen any related events, and clearly state the names of anybody you are reporting. If you asked a question, please ensure it was clear what you were asking.", "Your [ticket_name] has now been closed.") return ..() /datum/controller/subsystem/tickets/fire() var/stales = checkStaleness() if(LAZYLEN(stales)) var/report for(var/num in stales) report += "[num], " message_staff("Tickets [report] have been open for over [TICKET_TIMEOUT / 600] minutes. Changing status to stale.") /datum/controller/subsystem/tickets/stat_entry() ..("Tickets: [LAZYLEN(allTickets)]") /datum/controller/subsystem/tickets/proc/checkStaleness() var/stales = list() for(var/T in allTickets) var/datum/ticket/ticket = T if(!(ticket.ticketState == TICKET_OPEN)) continue if(world.time > ticket.timeUntilStale && (!ticket.lastStaffResponse || !ticket.staffAssigned)) var/id = ticket.makeStale() stales += id return stales //Return the current ticket number ready to be called off. /datum/controller/subsystem/tickets/proc/getTicketCounter() return ticketCounter //Return the ticket counter and increment /datum/controller/subsystem/tickets/proc/getTicketCounterAndInc() . = ticketCounter ticketCounter++ return /datum/controller/subsystem/tickets/proc/resolveAllOpenTickets() // Resolve all open tickets for(var/i in allTickets) var/datum/ticket/T = i resolveTicket(T.ticketNum) //Open a new ticket and populate details then add to the list of open tickets /datum/controller/subsystem/tickets/proc/newTicket(client/C, passedContent, title) if(!C || !passedContent) return //Check if the user has an open ticket already within the cooldown period, if so we don't create a new one and re-set the cooldown period var/datum/ticket/existingTicket = checkForOpenTicket(C) if(existingTicket) existingTicket.setCooldownPeriod() to_chat(C.mob, "Your [ticket_name] #[existingTicket.ticketNum] remains open! Visit \"My tickets\" under the Admin Tab to view it.") return if(!title) title = passedContent var/datum/ticket/T = new(title, passedContent, getTicketCounterAndInc()) allTickets += T T.clientName = C T.locationSent = C.mob.loc.name T.mobControlled = C.mob //Inform the user that they have opened a ticket to_chat(C, "You have opened [ticket_name] number #[(getTicketCounter() - 1)]! Please be patient and we will help you soon!") var/ticket_open_sound = sound('sound/effects/adminticketopen.ogg') SEND_SOUND(C, ticket_open_sound) //Set ticket state with key N to open /datum/controller/subsystem/tickets/proc/openTicket(N) var/datum/ticket/T = allTickets[N] if(T.ticketState != TICKET_OPEN) message_staff("[usr.client] / ([usr]) re-opened [ticket_name] number [N]") T.ticketState = TICKET_OPEN return TRUE //Set ticket state with key N to resolved /datum/controller/subsystem/tickets/proc/resolveTicket(N) var/datum/ticket/T = allTickets[N] if(T.ticketState != TICKET_RESOLVED) T.ticketState = TICKET_RESOLVED message_staff("[usr.client] / ([usr]) resolved [ticket_name] number [N]") to_chat_safe(returnClient(N), "Your [ticket_name] has now been resolved.") return TRUE /datum/controller/subsystem/tickets/proc/convert_to_other_ticket(ticketId) if(!check_rights(rights_needed)) return if(alert("Are you sure to convert this ticket to an '[other_ticket_name]' ticket?",,"Yes","No") != "Yes") return if(!other_ticket_system_staff_check()) return var/datum/ticket/T = allTickets[ticketId] convert_ticket(T) /datum/controller/subsystem/tickets/proc/other_ticket_system_staff_check() var/list/staff = staff_countup(other_ticket_permission) if(!staff[1]) if(alert("No active staff online to answer the ticket. Are you sure you want to convert the ticket?",, "No", "Yes") != "Yes") return FALSE return TRUE /datum/controller/subsystem/tickets/proc/convert_ticket(datum/ticket/T) T.ticketState = TICKET_CLOSED var/client/C = usr.client to_chat_safe(T.clientName, list("[key_name_hidden(C)] has converted your ticket to a [other_ticket_name] ticket.",\ "Be sure to use the correct type of help next time!")) message_staff("[C] has converted ticket number [T.ticketNum] to a [other_ticket_name] ticket.") log_game("[C] has converted ticket number [T.ticketNum] to a [other_ticket_name] ticket.") create_other_system_ticket(T) /datum/controller/subsystem/tickets/proc/create_other_system_ticket(datum/ticket/T) SSmentor_tickets.newTicket(T.clientName, T.content, T.title) /datum/controller/subsystem/tickets/proc/autoRespond(N) if(!check_rights(rights_needed)) return var/datum/ticket/T = allTickets[N] var/client/C = usr.client if((T.staffAssigned && T.staffAssigned != C) || (T.lastStaffResponse && T.lastStaffResponse != C) || ((T.ticketState != TICKET_OPEN) && (T.ticketState != TICKET_STALE))) //if someone took this ticket, is it the same admin who is autoresponding? if so, then skip the warning if(alert(usr, "[T.ticketState == TICKET_OPEN ? "Another admin appears to already be handling this." : "This ticket is already marked as closed or resolved"] Are you sure you want to continue?", "Confirmation", "Yes", "No") != "Yes") return T.assignStaff(C) var/response_phrases = list("Thanks" = "Thanks, have a Paradise day!", "Handling It" = "The issue is being looked into, thanks.", "Already Resolved" = "The problem has been resolved already.", "Mentorhelp" = "Please redirect your question to Mentorhelp, as they are better experienced with these types of questions.", "Happens Again" = "Thanks, let us know if it continues to happen.", "Github Issue Report" = "To report a bug, please go to our Github page. Then go to 'Issues'. Then 'New Issue'. Then fill out the report form. If the report would reveal current-round information, file it after the round ends.", "Clear Cache" = "To fix a blank screen, go to the 'Special Verbs' tab and press 'Reload UI Resources'. If that fails, clear your BYOND cache (instructions provided with 'Reload UI Resources'). If that still fails, please adminhelp again, stating you have already done the following." , "IC Issue" = "This is an In Character (IC) issue and will not be handled by admins. You could speak to Security, Internal Affairs, a Departmental Head, Nanotrasen Representetive, or any other relevant authority currently on station.", "Reject" = "Reject", "Man Up" = "Man Up", "Appeal on the Forums" = "Appealing a ban must occur on the forums. Privately messaging, or adminhelping about your ban will not resolve it. To appeal your ban, please head to [config.banappeals]" ) var/sorted_responses = list() for(var/key in response_phrases) //build a new list based on the short descriptive keys of the master list so we can send this as the input instead of the full paragraphs to the admin choosing which autoresponse sorted_responses += key var/message_key = input("Select an autoresponse. This will mark the ticket as resolved.", "Autoresponse") as null|anything in sortTim(sorted_responses, /proc/cmp_text_asc) //use sortTim and cmp_text_asc to sort alphabetically switch(message_key) if(null) //they cancelled T.staffAssigned = initial(T.staffAssigned) //if they cancel we dont need to hold this ticket anymore return if("Reject") if(!closeTicket(N)) to_chat(C, "Unable to close ticket") if("Man Up") C.man_up(returnClient(N)) T.lastStaffResponse = "Autoresponse: [message_key]" resolveTicket(N) message_staff("[C] has auto responded to [T.clientName]\'s adminhelp with: [message_key] ") log_game("[C] has auto responded to [T.clientName]\'s adminhelp with: [response_phrases[message_key]]") if("Mentorhelp") convert_ticket(T) else var/msg_sound = sound('sound/effects/adminhelp.ogg') SEND_SOUND(returnClient(N), msg_sound) to_chat_safe(returnClient(N), "[key_name_hidden(C)] is autoresponding with: [response_phrases[message_key]]")//for this we want the full value of whatever key this is to tell the player so we do response_phrases[message_key] message_staff("[C] has auto responded to [T.clientName]\'s adminhelp with: [message_key] ") //we want to use the short named keys for this instead of the full sentence which is why we just do message_key T.lastStaffResponse = "Autoresponse: [message_key]" resolveTicket(N) log_game("[C] has auto responded to [T.clientName]\'s adminhelp with: [response_phrases[message_key]]") //Set ticket state with key N to closed /datum/controller/subsystem/tickets/proc/closeTicket(N) var/datum/ticket/T = allTickets[N] if(T.ticketState != TICKET_CLOSED) message_staff("[usr.client] / ([usr]) closed [ticket_name] number [N]") to_chat_safe(returnClient(N), close_messages) T.ticketState = TICKET_CLOSED return TRUE //Check if the user already has a ticket open and within the cooldown period. /datum/controller/subsystem/tickets/proc/checkForOpenTicket(client/C) for(var/datum/ticket/T in allTickets) if(T.clientName == C && T.ticketState == TICKET_OPEN && (T.ticketCooldown > world.time)) return T return FALSE //Check if the user has ANY ticket not resolved or closed. /datum/controller/subsystem/tickets/proc/checkForTicket(client/C) var/list/tickets = list() for(var/datum/ticket/T in allTickets) if(T.clientName == C && (T.ticketState == TICKET_OPEN || T.ticketState == TICKET_STALE)) tickets += T if(tickets.len) return tickets return FALSE //return the client of a ticket number /datum/controller/subsystem/tickets/proc/returnClient(N) var/datum/ticket/T = allTickets[N] return T.clientName /datum/controller/subsystem/tickets/proc/assignStaffToTicket(client/C, N) var/datum/ticket/T = allTickets[N] if(T.staffAssigned != null && T.staffAssigned != C && alert("Ticket is already assigned to [T.staffAssigned.ckey]. Are you sure you want to take it?","Take ticket","No","Yes") != "Yes") return FALSE T.assignStaff(C) return TRUE //Single staff ticket /datum/ticket var/ticketNum // Ticket number var/clientName // Client which opened the ticket var/timeOpened // Time the ticket was opened var/title //The initial message with links var/list/content // content of the staff help var/lastStaffResponse // Last staff member who responded var/lastResponseTime // When the staff last responded var/locationSent // Location the player was when they send the ticket var/mobControlled // Mob they were controlling var/ticketState // State of the ticket, open, closed, resolved etc var/timeUntilStale // When the ticket goes stale var/ticketCooldown // Cooldown before allowing the user to open another ticket. var/client/staffAssigned // Staff member who has assigned themselves to this ticket /datum/ticket/New(tit, cont, num) title = tit content = list() content += cont timeOpened = worldtime2text() timeUntilStale = world.time + TICKET_TIMEOUT setCooldownPeriod() ticketNum = num ticketState = TICKET_OPEN //Set the cooldown period for the ticket. The time when it's created plus the defined cooldown time. /datum/ticket/proc/setCooldownPeriod() ticketCooldown = world.time + TICKET_DUPLICATE_COOLDOWN //Set the last staff who responded as the client passed as an arguement. /datum/ticket/proc/setLastStaffResponse(client/C) lastStaffResponse = C lastResponseTime = worldtime2text() //Return the ticket state as a colour coded text string. /datum/ticket/proc/state2text() switch(ticketState) if(TICKET_OPEN) return "OPEN" if(TICKET_RESOLVED) return "RESOLVED" if(TICKET_CLOSED) return "CLOSED" if(TICKET_STALE) return "STALE" //Assign the client passed to var/staffAsssigned /datum/ticket/proc/assignStaff(client/C) if(!C) return staffAssigned = C return TRUE /datum/ticket/proc/addResponse(client/C, msg) if(C.holder) setLastStaffResponse(C) msg = "[C]: [msg]" content += msg /datum/ticket/proc/makeStale() ticketState = TICKET_STALE return ticketNum /* UI STUFF */ /datum/controller/subsystem/tickets/proc/returnUI(tab = TICKET_OPEN) set name = "Open Ticket Interface" set category = "Tickets" //dat var/trStyle = "border-top:2px solid; border-bottom:2px solid; padding-top: 5px; padding-bottom: 5px;" var/tdStyleleft = "border-top:2px solid; border-bottom:2px solid; width:150px; text-align:center;" var/tdStyle = "border-top:2px solid; border-bottom:2px solid;" var/datum/ticket/ticket var/dat dat += "" dat += "

[ticket_system_name]

" dat +="Refresh
Open TicketsResolved TicketsClosed Tickets" if(tab == TICKET_OPEN) dat += "

Open Tickets

" dat += "" dat +="" if(tab == TICKET_OPEN) for(var/T in allTickets) ticket = T if(ticket.ticketState == TICKET_OPEN || ticket.ticketState == TICKET_STALE) dat += "" else continue else if(tab == TICKET_RESOLVED) dat += "

Resolved Tickets

" for(var/T in allTickets) ticket = T if(ticket.ticketState == TICKET_RESOLVED) dat += "" else continue else if(tab == TICKET_CLOSED) dat += "

Closed Tickets

" for(var/T in allTickets) ticket = T if(ticket.ticketState == TICKET_CLOSED) dat += "" else continue dat += "
ControlTicket
ResolveDetails
#[ticket.ticketNum] ([ticket.timeOpened]) [ticket.ticketState == TICKET_STALE ? "STALE" : ""]
[ticket.title]
ResolveDetails
#[ticket.ticketNum] ([ticket.timeOpened])
[ticket.title]
ResolveDetails
#[ticket.ticketNum] ([ticket.timeOpened])
[ticket.title]
" dat += "

Resolve All

" if(ticket_system_name == "Mentor Tickets") dat += "Resolve All Open Mentor Tickets" else dat += "Resolve All Open Admin Tickets" return dat /datum/controller/subsystem/tickets/proc/showUI(mob/user, tab) var/dat = null dat = returnUI(tab) var/datum/browser/popup = new(user, ticket_system_name, ticket_system_name, 1400, 600) popup.set_content(dat) popup.open() /datum/controller/subsystem/tickets/proc/showDetailUI(mob/user, ticketID) var/datum/ticket/T = allTickets[ticketID] var/status = "[T.state2text()]" var/dat = "

[ticket_system_name]

" dat +="Show AllRefresh" dat += "

Ticket #[T.ticketNum]

" dat += "

[T.clientName] / [T.mobControlled] opened this [ticket_name] at [T.timeOpened] at location [T.locationSent]

" dat += "

Ticket Status: [status]" dat += "" dat += "" if(T.content.len > 1) for(var/i = 2, i <= T.content.len, i++) dat += "" dat += "
[T.title]
[T.content[i]]


" dat += "Re-Open[check_rights(rights_needed, 0) ? "Auto": ""]Resolve

" if(!T.staffAssigned) dat += "No staff member assigned to this [ticket_name] - Take Ticket
" else dat += "[T.staffAssigned] is assigned to this Ticket. - Take Ticket - Unassign Ticket
" if(T.lastStaffResponse) dat += "Last Staff response Response: [T.lastStaffResponse] at [T.lastResponseTime]" else dat +="No Staff Response" dat += "

" dat += "Close Ticket" dat += "Convert Ticket" var/datum/browser/popup = new(user, "[ticket_system_name]detail", "[ticket_system_name] #[T.ticketNum]", 1000, 600) popup.set_content(dat) popup.open() /datum/controller/subsystem/tickets/proc/userDetailUI(mob/user) //dat var/tickets = checkForTicket(user.client) var/dat dat += "

Your open [ticket_system_name]

" dat += "" for(var/datum/ticket/T in tickets) dat += "" for(var/i = 1, i <= T.content.len, i++) dat += "" dat += "

Ticket #[T.ticketNum]

[T.content[i]]
" var/datum/browser/popup = new(user, "[ticket_system_name]userticketsdetail", ticket_system_name, 1000, 600) popup.set_content(dat) popup.open() //Sends a message to the target safely. If the target left the server it won't throw a runtime. Also accepts lists of text /datum/controller/subsystem/tickets/proc/to_chat_safe(target, text) if(!target) return FALSE if(istype(text, /list)) for(var/T in text) to_chat(target, T) else to_chat(target, text) return TRUE //Sends a message to the designated staff /datum/controller/subsystem/tickets/proc/message_staff(var/msg, var/alt = FALSE) message_adminTicket(msg, alt) /datum/controller/subsystem/tickets/Topic(href, href_list) if(href_list["refresh"]) showUI(usr) return if(href_list["refreshdetail"]) var/indexNum = text2num(href_list["refreshdetail"]) showDetailUI(usr, indexNum) return if(href_list["showopen"]) showUI(usr, TICKET_OPEN) return if(href_list["showresolved"]) showUI(usr, TICKET_RESOLVED) return if(href_list["showclosed"]) showUI(usr, TICKET_CLOSED) return if(href_list["details"]) var/indexNum = text2num(href_list["details"]) showDetailUI(usr, indexNum) return if(href_list["resolve"]) var/indexNum = text2num(href_list["resolve"]) if(resolveTicket(indexNum)) showUI(usr) if(href_list["detailresolve"]) var/indexNum = text2num(href_list["detailresolve"]) if(resolveTicket(indexNum)) showDetailUI(usr, indexNum) if(href_list["detailclose"]) var/indexNum = text2num(href_list["detailclose"]) if(!check_rights(close_rights)) to_chat(usr, "Not enough rights to close this ticket.") return if(alert("Are you sure? This will send a negative message.",,"Yes","No") != "Yes") return if(closeTicket(indexNum)) showDetailUI(usr, indexNum) if(href_list["detailreopen"]) var/indexNum = text2num(href_list["detailreopen"]) if(openTicket(indexNum)) showDetailUI(usr, indexNum) if(href_list["assignstaff"]) var/indexNum = text2num(href_list["assignstaff"]) takeTicket(indexNum) showDetailUI(usr, indexNum) if(href_list["unassignstaff"]) var/indexNum = text2num(href_list["unassignstaff"]) unassignTicket(indexNum) showDetailUI(usr, indexNum) if(href_list["autorespond"]) var/indexNum = text2num(href_list["autorespond"]) autoRespond(indexNum) if(href_list["convert_ticket"]) var/indexNum = text2num(href_list["convert_ticket"]) convert_to_other_ticket(indexNum) if(href_list["resolveall"]) if(ticket_system_name == "Mentor Tickets") usr.client.resolveAllMentorTickets() else usr.client.resolveAllAdminTickets() /datum/controller/subsystem/tickets/proc/takeTicket(var/index) if(assignStaffToTicket(usr.client, index)) if(span_class == "mentorhelp") message_staff("[usr.client] / ([usr]) has taken [ticket_name] number [index]") else message_staff("[usr.client] / ([usr]) has taken [ticket_name] number [index]", TRUE) to_chat_safe(returnClient(index), "Your [ticket_name] is being handled by [usr.client].") /datum/controller/subsystem/tickets/proc/unassignTicket(index) var/datum/ticket/T = allTickets[index] if(T.staffAssigned != null && (T.staffAssigned == usr.client || alert("Ticket is already assigned to [T.staffAssigned]. Do you want to unassign it?","Unassign ticket","No","Yes") == "Yes")) T.staffAssigned = null to_chat_safe(returnClient(index), "Your [ticket_name] has been unassigned. Another staff member will help you soon.") if(span_class == "mentorhelp") message_staff("[usr.client] / ([usr]) has unassigned [ticket_name] number [index]") else message_staff("[usr.client] / ([usr]) has unassigned [ticket_name] number [index]", TRUE)