From 685010c8667318f13a2e1de873a927540c0ee042 Mon Sep 17 00:00:00 2001 From: Poojawa Date: Sun, 30 Sep 2018 11:17:12 -0500 Subject: [PATCH] admin helps, but working this time. --- code/__defines/admin.dm | 24 + code/__defines/qdel.dm | 9 +- code/__defines/typeids.dm | 6 + code/_global_vars/misc.dm | 6 +- code/_global_vars/mobs.dm | 5 +- code/_helpers/game.dm | 25 +- code/_helpers/lists.dm | 11 + code/_helpers/time.dm | 104 +++- code/_helpers/unsorted.dm | 10 +- code/modules/admin/admin_verbs.dm | 23 +- code/modules/admin/topic.dm | 22 +- code/modules/admin/verbs/adminhelp.dm | 620 ++++++++++++++++++++++-- code/modules/admin/verbs/adminjump.dm | 18 +- code/modules/admin/verbs/adminpm.dm | 327 +++++++++---- code/modules/admin/verbs/randomverbs.dm | 18 +- code/modules/client/client defines.dm | 1 + code/modules/client/client procs.dm | 5 +- code/modules/mob/mob.dm | 5 +- code/world.dm | 2 + interface/interface.dm | 2 +- polaris.dme | 1 + 21 files changed, 1069 insertions(+), 175 deletions(-) create mode 100644 code/__defines/typeids.dm diff --git a/code/__defines/admin.dm b/code/__defines/admin.dm index d2afb7846e..84e0656c97 100644 --- a/code/__defines/admin.dm +++ b/code/__defines/admin.dm @@ -45,3 +45,27 @@ #define SMITE_BLUESPACEARTILLERY "Bluespace Artillery" #define SMITE_SPONTANEOUSCOMBUSTION "Spontaneous Combustion" #define SMITE_LIGHTNINGBOLT "Lightning Bolt" + +#define ADMIN_QUE(user) "(?)" +#define ADMIN_FLW(user) "(FLW)" +#define ADMIN_PP(user) "(PP)" +#define ADMIN_VV(atom) "(VV)" +#define ADMIN_SM(user) "(SM)" +#define ADMIN_TP(user) "(TP)" +#define ADMIN_BSA(user) "(BSA)" +#define ADMIN_KICK(user) "(KICK)" +#define ADMIN_CENTCOM_REPLY(user) "(RPLY)" +#define ADMIN_SYNDICATE_REPLY(user) "(RPLY)" +#define ADMIN_SC(user) "(SC)" +#define ADMIN_SMITE(user) "(SMITE)" +#define ADMIN_LOOKUP(user) "[key_name_admin(user)][ADMIN_QUE(user)]" +#define ADMIN_LOOKUPFLW(user) "[key_name_admin(user)][ADMIN_QUE(user)] [ADMIN_FLW(user)]" +#define ADMIN_FULLMONTY_NONAME(user) "[ADMIN_QUE(user)] [ADMIN_PP(user)] [ADMIN_VV(user)] [ADMIN_SM(user)] [ADMIN_FLW(user)] [ADMIN_TP(user)]" +#define ADMIN_FULLMONTY(user) "[key_name_admin(user)] [ADMIN_FULLMONTY_NONAME(user)]" +#define ADMIN_JMP(src) "(JMP)" +#define COORD(src) "[src ? "([src.x],[src.y],[src.z])" : "nonexistent location"]" +#define ADMIN_COORDJMP(src) "[src ? "[COORD(src)] [ADMIN_JMP(src)]" : "nonexistent location"]" + +#define AHELP_ACTIVE 1 +#define AHELP_CLOSED 2 +#define AHELP_RESOLVED 3 \ No newline at end of file diff --git a/code/__defines/qdel.dm b/code/__defines/qdel.dm index 82ec12a211..51c6db5325 100644 --- a/code/__defines/qdel.dm +++ b/code/__defines/qdel.dm @@ -20,4 +20,11 @@ #define QDELING(X) (X.gc_destroyed) #define QDELETED(X) (!X || X.gc_destroyed) -#define QDESTROYING(X) (!X || X.gc_destroyed == GC_CURRENTLY_BEING_QDELETED) \ No newline at end of file +#define QDESTROYING(X) (!X || X.gc_destroyed == GC_CURRENTLY_BEING_QDELETED) + +#define QDEL_LIST(L) if(L) { for(var/I in L) qdel(I); L.Cut(); } +#define QDEL_LIST_ASSOC(L) if(L) { for(var/I in L) { qdel(L[I]); qdel(I); } L.Cut(); } +#define QDEL_LIST_ASSOC_VAL(L) if(L) { for(var/I in L) qdel(L[I]); L.Cut(); } + +/proc/______qdel_list_wrapper(list/L) //the underscores are to encourage people not to use this directly. + QDEL_LIST(L) diff --git a/code/__defines/typeids.dm b/code/__defines/typeids.dm new file mode 100644 index 0000000000..9af310012b --- /dev/null +++ b/code/__defines/typeids.dm @@ -0,0 +1,6 @@ +//Byond type ids +#define TYPEID_NULL "0" +#define TYPEID_NORMAL_LIST "f" +//helper macros +#define GET_TYPEID(ref) ( ( (lentext(ref) <= 10) ? "TYPEID_NULL" : copytext(ref, 4, lentext(ref)-6) ) ) +#define IS_NORMAL_LIST(L) (GET_TYPEID("\ref[L]") == TYPEID_NORMAL_LIST) \ No newline at end of file diff --git a/code/_global_vars/misc.dm b/code/_global_vars/misc.dm index de2779c45b..5e7737f163 100644 --- a/code/_global_vars/misc.dm +++ b/code/_global_vars/misc.dm @@ -1 +1,5 @@ -GLOBAL_LIST_EMPTY(all_observable_events) \ No newline at end of file +GLOBAL_LIST_EMPTY(all_observable_events) + +GLOBAL_VAR_INIT(timezoneOffset, 0) // The difference betwen midnight (of the host computer) and 0 world.ticks. + +GLOBAL_VAR_INIT(TAB, "    ") \ No newline at end of file diff --git a/code/_global_vars/mobs.dm b/code/_global_vars/mobs.dm index 7fa2b5d162..fac5a14036 100644 --- a/code/_global_vars/mobs.dm +++ b/code/_global_vars/mobs.dm @@ -1,2 +1,5 @@ GLOBAL_LIST_EMPTY(admins) //all clients whom are admins -GLOBAL_PROTECT(admins) \ No newline at end of file +GLOBAL_PROTECT(admins) +GLOBAL_LIST_EMPTY(deadmins) //all ckeys who have used the de-admin verb. +GLOBAL_LIST_EMPTY(stealthminID) +GLOBAL_LIST_EMPTY(directory) //all ckeys with associated client \ No newline at end of file diff --git a/code/_helpers/game.dm b/code/_helpers/game.dm index a9ac87e4aa..9ba432779f 100644 --- a/code/_helpers/game.dm +++ b/code/_helpers/game.dm @@ -26,17 +26,17 @@ max_z = max(z, max_z) return max_z -/proc/get_area(O) - var/turf/loc = get_turf(O) - if(loc) - var/area/res = loc.loc - .= res +/proc/get_area(atom/A) + if(isarea(A)) + return A + var/turf/T = get_turf(A) + return T ? T.loc : null -/proc/get_area_name(N) //get area by its name - for(var/area/A in all_areas) - if(A.name == N) - return A - return 0 +/proc/get_area_name(atom/X, format_text = FALSE) + var/area/A = isarea(X) ? X : get_area(X) + if(!A) + return null + return format_text ? format_text(A.name) : A.name /proc/get_area_master(const/O) var/area/A = get_area(O) @@ -602,3 +602,8 @@ datum/projectile_data /proc/SecondsToTicks(var/seconds) return seconds * 10 + +/proc/window_flash(var/client_or_usr) + if (!client_or_usr) + return + winset(client_or_usr, "mainwindow", "flash=5") \ No newline at end of file diff --git a/code/_helpers/lists.dm b/code/_helpers/lists.dm index 17bc605ae7..b9e5dd8623 100644 --- a/code/_helpers/lists.dm +++ b/code/_helpers/lists.dm @@ -240,6 +240,17 @@ proc/listclearnulls(list/list) for(var/i in L) . |= i +//same, but returns nothing and acts on list in place (also handles associated values properly) +/proc/uniqueList_inplace(list/L) + var/temp = L.Copy() + L.len = 0 + for(var/key in temp) + if (isnum(key)) + L |= key + else + L[key] = temp[key] + + //Mergesort: divides up the list into halves to begin the sort /proc/sortKey(var/list/client/L, var/order = 1) if(isnull(L) || L.len < 2) diff --git a/code/_helpers/time.dm b/code/_helpers/time.dm index f75802eb45..ce640731a6 100644 --- a/code/_helpers/time.dm +++ b/code/_helpers/time.dm @@ -64,6 +64,11 @@ var/next_station_date_change = 1 DAY var/time_portion = time2text(world.timeofday, "hh:mm:ss") return "[date_portion]T[time_portion]" +/proc/gameTimestamp(format = "hh:mm:ss", wtime=null) + if(!wtime) + wtime = world.time + return time2text(wtime - GLOB.timezoneOffset, format) + /* Returns 1 if it is the selected month and day */ proc/isDay(var/month, var/day) if(isnum(month) && isnum(day)) @@ -137,4 +142,101 @@ var/round_start_time = 0 i *= 2 while (TICK_USAGE > min(TICK_LIMIT_TO_RUN, GLOB.CURRENT_TICKLIMIT)) -#undef DELTA_CALC \ No newline at end of file +#undef DELTA_CALC + + +//Takes a value of time in deciseconds. +//Returns a text value of that number in hours, minutes, or seconds. +/proc/DisplayTimeText(time_value, truncate = FALSE) + var/second = (time_value)*0.1 + var/second_adjusted = null + var/second_rounded = FALSE + var/minute = null + var/hour = null + var/day = null + + if(!second) + return "0 seconds" + if(second >= 60) + minute = FLOOR(second/60, 1) + second = round(second - (minute*60), 0.1) + second_rounded = TRUE + if(second) //check if we still have seconds remaining to format, or if everything went into minute. + second_adjusted = round(second) //used to prevent '1 seconds' being shown + if(day || hour || minute) + if(second_adjusted == 1 && second >= 1) + second = " and 1 second" + else if(second > 1) + second = " and [second_adjusted] seconds" + else //shows a fraction if seconds is < 1 + if(second_rounded) //no sense rounding again if it's already done + second = " and [second] seconds" + else + second = " and [round(second, 0.1)] seconds" + else + if(second_adjusted == 1 && second >= 1) + second = "[truncate ? "second" : "1 second"]" + else if(second > 1) + second = "[second_adjusted] seconds" + else + if(second_rounded) + second = "[second] seconds" + else + second = "[round(second, 0.1)] seconds" + else + second = null + + if(!minute) + return "[second]" + if(minute >= 60) + hour = FLOOR(minute/60, 1) + minute = (minute - (hour*60)) + if(minute) //alot simpler from here since you don't have to worry about fractions + if(minute != 1) + if((day || hour) && second) + minute = ", [minute] minutes" + else if((day || hour) && !second) + minute = " and [minute] minutes" + else + minute = "[minute] minutes" + else + if((day || hour) && second) + minute = ", 1 minute" + else if((day || hour) && !second) + minute = " and 1 minute" + else + minute = "[truncate ? "minute" : "1 minute"]" + else + minute = null + + if(!hour) + return "[minute][second]" + if(hour >= 24) + day = FLOOR(hour/24, 1) + hour = (hour - (day*24)) + if(hour) + if(hour != 1) + if(day && (minute || second)) + hour = ", [hour] hours" + else if(day && (!minute || !second)) + hour = " and [hour] hours" + else + hour = "[hour] hours" + else + if(day && (minute || second)) + hour = ", 1 hour" + else if(day && (!minute || !second)) + hour = " and 1 hour" + else + hour = "[truncate ? "hour" : "1 hour"]" + else + hour = null + + if(!day) + return "[hour][minute][second]" + if(day > 1) + day = "[day] days" + else + day = "[truncate ? "day" : "1 day"]" + + return "[day][hour][minute][second]" \ No newline at end of file diff --git a/code/_helpers/unsorted.dm b/code/_helpers/unsorted.dm index 91720600dd..4f9e7d00b1 100644 --- a/code/_helpers/unsorted.dm +++ b/code/_helpers/unsorted.dm @@ -519,6 +519,14 @@ Turf and target are seperate in case you want to teleport some distance from a t return "[round((powerused * 0.000001),0.001)] MW" return "[round((powerused * 0.000000001),0.0001)] GW" +/proc/get_mob_by_ckey(key) + if(!key) + return + var/list/mobs = sortmobs() + for(var/mob/M in mobs) + if(M.ckey == key) + return M + //Forces a variable to be posative /proc/modulus(var/M) if(M >= 0) @@ -1429,4 +1437,4 @@ var/mob/dview/dview_mob = new return "North-Northwest" /proc/pass() - return \ No newline at end of file + return diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 56a440b639..dd47534dbe 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -585,6 +585,25 @@ var/list/admin_verbs_event_manager = list( feedback_add_details("admin_verb","OC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! return +/client/proc/findStealthKey(txt) + if(txt) + for(var/P in GLOB.stealthminID) + if(GLOB.stealthminID[P] == txt) + return P + txt = GLOB.stealthminID[ckey] + return txt + +/client/proc/createStealthKey() + var/num = (rand(0,1000)) + var/i = 0 + while(i == 0) + i = 1 + for(var/P in GLOB.stealthminID) + if(num == GLOB.stealthminID[P]) + num++ + i = 0 + GLOB.stealthminID["[ckey]"] = "@[num2text(num)]" + /client/proc/stealth() set category = "Admin" set name = "Stealth Mode" @@ -595,10 +614,12 @@ var/list/admin_verbs_event_manager = list( mob.name = capitalize(ckey) else var/new_key = ckeyEx(input("Enter your desired display name.", "Fake Key", key) as text|null) - if(!new_key) return + if(!new_key) + return if(length(new_key) >= 26) new_key = copytext(new_key, 1, 26) holder.fakekey = new_key + createStealthKey() if(istype(mob, /mob/new_player)) mob.name = new_key log_admin("[key_name(usr)] has turned stealth mode [holder.fakekey ? "ON" : "OFF"]") diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 28470c376e..0c53e8492c 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -10,6 +10,20 @@ check_antagonists() return + if(href_list["ahelp"]) + if(!check_rights(R_ADMIN)) + return + + var/ahelp_ref = href_list["ahelp"] + var/datum/admin_help/AH = locate(ahelp_ref) + if(AH) + AH.Action(href_list["ahelp_action"]) + else + to_chat(usr, "Ticket [ahelp_ref] has been deleted!") + + else if(href_list["ahelp_tickets"]) + GLOB.ahelp_tickets.BrowseTickets(text2num(href_list["ahelp_tickets"])) + if(href_list["dbsearchckey"] || href_list["dbsearchadmin"]) var/adminckey = href_list["dbsearchadmin"] @@ -841,7 +855,9 @@ M << "No ban appeals URL has been set." log_admin("[usr.client.ckey] has banned [M.ckey].\nReason: [reason]\nThis will be removed in [mins] minutes.") message_admins("[usr.client.ckey] has banned [M.ckey].\nReason: [reason]\nThis will be removed in [mins] minutes.") - + var/datum/admin_help/AH = M.client ? M.client.current_ticket : null + if(AH) + AH.Resolve() qdel(M.client) //qdel(M) // See no reason why to delete mob. Important stuff can be lost. And ban can be lifted before round ends. if("No") @@ -867,7 +883,9 @@ message_admins("[usr.client.ckey] has banned [M.ckey].\nReason: [reason]\nThis is a permanent ban.") feedback_inc("ban_perma",1) DB_ban_record(BANTYPE_PERMA, M, -1, reason) - + var/datum/admin_help/AH = M.client ? M.client.current_ticket : null + if(AH) + AH.Resolve() qdel(M.client) //qdel(M) if("Cancel") diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 766473e010..819cc3dc7b 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -1,32 +1,574 @@ +/client/var/datum/admin_help/current_ticket //the current ticket the (usually) not-admin client is dealing with -//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") +// +//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 ..() + +//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.AddInteraction("Client reconnected.") + C.current_ticket.initiator = C + +//Dissasociate ticket +/datum/admin_help_tickets/proc/ClientLogout(client/C) + if(C.current_ticket) + C.current_ticket.AddInteraction("Client disconnected.") + 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/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(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 + log_debug("Multiple ahelp current_tickets") + initiator.current_ticket.AddInteraction("Ticket erroneously left open by code") + initiator.current_ticket.Close() + initiator.current_ticket = src + + var/parsed_message = keywords_lookup(msg) + + 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(parsed_message) + + //show it to the person adminhelping too + to_chat(C, "PM to-Admins: [name]") + + //send it to irc if nobody is on and tell us how many were on + var/admin_number_present = send2irc_adminless_only(initiator_ckey, name) + log_admin("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 to the admin irc.") + + 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) + _interactions += "[gameTimestamp()]: [formatted_message]" + +//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 +/datum/admin_help/proc/MessageNoRecipient(msg) + var/ref_src = "\ref[src]" + var/chat_msg = "Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]: [msg]" + + AddInteraction("[LinkedReplyName(ref_src)]: [msg]") + //send this msg to all admins + + for(var/client/X in GLOB.admins) + if(X.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) + X << 'sound/effects/adminhelp.ogg' + window_flash(X) + to_chat(X, chat_msg) + +//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) + feedback_dec("ahelp_close") + if(AHELP_RESOLVED) + feedback_dec("ahelp_resolve") + 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(msg) + feedback_inc("ahelp_reopen") + 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) + feedback_inc("ahelp_close") + var/msg = "Ticket [TicketHref("#[id]")] closed by [key_name]." + message_admins(msg) + log_admin(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) + + AddInteraction("Resolved by [key_name].") + if(!silent) + feedback_inc("ahelp_resolve") + var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name]" + message_admins(msg) + log_admin(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 << '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.") + + feedback_inc("ahelp_reject") + var/msg = "Ticket [TicketHref("#[id]")] rejected by [key_name]" + message_admins(msg) + log_admin(msg) + AddInteraction("Rejected by [key_name].") + 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 += "Losing is part of the game!
" + msg += "Your character will frequently die, sometimes without even a possibility of avoiding it. Events will often be out of your control. No matter how good or prepared you are, sometimes you just lose." + + if(initiator) + to_chat(initiator, msg) + + feedback_inc("ahelp_icissue") + msg = "Ticket [TicketHref("#[id]")] marked as IC by [key_name]" + message_admins(msg) + log_admin(msg) + AddInteraction("Marked as IC issue by [key_name]") + 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 += "[GLOB.TAB][TicketHref("Refresh", ref_src)][GLOB.TAB][TicketHref("Re-Title", ref_src, "retitle")]" + if(state != AHELP_ACTIVE) + dat += "[GLOB.TAB][TicketHref("Reopen", ref_src, "reopen")]" + dat += "

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

" + if(initiator) + dat += "Actions: [FullMonty(ref_src)]
" + else + dat += "DISCONNECTED[GLOB.TAB][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(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/New(loc, datum/admin_help/AH) + ahelp_datum = AH + ..(loc) + +/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/verb/adminhelp(msg as text) set category = "Admin" set name = "Adminhelp" if(say_disabled) //This is here to try to identify lag problems - usr << "Speech is currently admin-disabled." + to_chat(usr, "Speech is currently admin-disabled.") return //handle muting and automuting if(prefs.muted & MUTE_ADMINHELP) - src << "Error: Admin-PM: You cannot send adminhelps (Muted)." + to_chat(src, "Error: Admin-PM: You cannot send adminhelps (Muted).") + return + if(handle_spam_prevention(msg,MUTE_ADMINHELP)) return - adminhelped = 1 //Determines if they get the message to reply by clicking the name. - - if(msg) - handle_spam_prevention(MUTE_ADMINHELP) - - //clean the input msg if(!msg) return - msg = sanitize(msg) - if(!msg) + + //remove out adminhelp verb temporarily to prevent spamming of admins. + src.verbs -= /client/verb/adminhelp + spawn(1200) + src.verbs += /client/verb/adminhelp // 2 minute cool-down for adminhelps + + feedback_add_details("admin_verb","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) + 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) + +//admin proc +/client/proc/cmd_admin_ticket_panel() + set name = "Show Ticket List" + set category = "Admin" + + if(!check_rights(R_ADMIN, TRUE)) return - var/original_msg = msg + + var/browse_to + + switch(input("Display which ticket list?") as null|anything in list("Active Tickets", "Closed Tickets", "Resolved Tickets")) + if("Active Tickets") + browse_to = AHELP_ACTIVE + if("Closed Tickets") + browse_to = AHELP_CLOSED + if("Resolved Tickets") + browse_to = AHELP_RESOLVED + else + return + + GLOB.ahelp_tickets.BrowseTickets(browse_to) + +// +// 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(X)) + .["noflags"] += X + else if(X.is_afk()) + .["afk"] += X + else if(X.holder.fakekey) + .["stealth"] += X + else + .["present"] += X + +/proc/send2irc_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] " + send2irc(source,final) + +/proc/ircadminwho() + 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(admin_keys) > 1) + message += ", [admin]" + else + message += "[admin]" + + return jointext(message, "") + +/proc/keywords_lookup(msg,irc) + + //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, " ") @@ -35,9 +577,11 @@ var/list/adminhelp_ignored_words = list("unknown","the","a","an","of","monkey"," var/list/surnames = list() var/list/forenames = list() var/list/ckeys = list() + var/founds = "" for(var/mob/M in mob_list) var/list/indexing = list(M.real_name, M.name) - if(M.mind) indexing += M.mind.name + if(M.mind) + indexing += M.mind.name for(var/string in indexing) var/list/L = splittext(string, " ") @@ -77,41 +621,17 @@ var/list/adminhelp_ignored_words = list("unknown","the","a","an","of","monkey"," mobs_found += found if(!ai_found && isAI(found)) ai_found = 1 - msg += "[original_word] (?) " + var/is_antag = 0 + if(found.mind && found.mind.special_role) + is_antag = 1 + founds += "Name: [found.name]([found.real_name]) Ckey: [found.ckey] [is_antag ? "(Antag)" : null] " + msg += "[original_word](?|F) " continue - msg += "[original_word] " - - if(!mob) //this doesn't happen - return - - var/ai_cl - if(ai_found) - ai_cl = " (CL)" - - //Options bar: mob, details ( admin = 2, dev = 3, event manager = 4, character name (0 = just ckey, 1 = ckey and character name), link? (0 no don't make it a link, 1 do so), - // highlight special roles (0 = everyone has same looking name, 1 = antags / special roles get a golden name) - - msg = "Request for Help: [get_options_bar(mob, 2, 1, 1)][ai_cl] [msg]" - - var/admin_number_afk = 0 - - for(var/client/X in admins) - if((R_ADMIN|R_MOD|R_EVENT|R_SERVER) & X.holder.rights) - if(X.is_afk()) - admin_number_afk++ - X << msg - if(X.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) - X << 'sound/effects/adminhelp.ogg' - - //show it to the person adminhelping too - src << "PM to-Staff : [original_msg]" - - var/admin_number_present = admins.len - admin_number_afk - log_admin("HELP: [key_name(src)]: [original_msg] - heard by [admin_number_present] non-AFK admins.") - if(admin_number_present <= 0) - send2adminirc("Request for Help from [key_name(src)]: [html_decode(original_msg)] - !![admin_number_afk ? "All admins AFK ([admin_number_afk])" : "No admins online"]!!") - else - send2adminirc("Request for Help from [key_name(src)]: [html_decode(original_msg)]") - feedback_add_details("admin_verb","AH") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - return + msg += "[original_word] " + if(irc) + if(founds == "") + return "Search Failed" + else + return founds + return msg \ No newline at end of file diff --git a/code/modules/admin/verbs/adminjump.dm b/code/modules/admin/verbs/adminjump.dm index 823a540d10..8e39f45d61 100644 --- a/code/modules/admin/verbs/adminjump.dm +++ b/code/modules/admin/verbs/adminjump.dm @@ -109,8 +109,10 @@ if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return if(config.allow_admin_jump) - log_admin("[key_name(usr)] teleported [key_name(M)]") - message_admins("[key_name_admin(usr)] teleported [key_name_admin(M)]", 1) + log_admin("[key_name(usr)] jumped to [key_name(M)]") + var/msg = "[key_name_admin(usr)] jumped to [key_name_admin(M)]" + message_admins(msg) + admin_ticket_log(M, msg) M.on_mob_jump() M.loc = get_turf(usr) feedback_add_details("admin_verb","GM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -137,7 +139,9 @@ if(!M) return log_admin("[key_name(usr)] teleported [key_name(M)]") - message_admins("[key_name_admin(usr)] teleported [key_name(M)]", 1) + var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)]" + message_admins(msg) + admin_ticket_log(M, msg) if(M) M.on_mob_jump() M.loc = get_turf(usr) @@ -157,7 +161,9 @@ M.loc = pick(get_area_turfs(A)) feedback_add_details("admin_verb","SMOB") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - log_admin("[key_name(usr)] teleported [key_name(M)] to [A]") - message_admins("[key_name_admin(usr)] teleported [key_name_admin(M)] to [A]", 1) + log_admin("[key_name(usr)] teleported [key_name(M)]") + var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)]" + message_admins(msg) + admin_ticket_log(M, msg) else - alert("Admin jumping disabled") + alert("Admin jumping disabled") \ No newline at end of file diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index 4ff94877f4..ec657f052f 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -1,145 +1,286 @@ +#define IRCREPLYCOUNT 2 + + //allows right clicking mobs to send an admin PM to their client, forwards the selected mob's client to cmd_admin_pm -/client/proc/cmd_admin_pm_context(mob/M as mob in mob_list) +/client/proc/cmd_admin_pm_context(mob/M in mob_list) set category = null set name = "Admin PM Mob" if(!holder) - src << "Error: Admin-PM-Context: Only administrators may use this command." + to_chat(src, "Error: Admin-PM-Context: Only administrators may use this command.") + return + if( !ismob(M) || !M.client ) return - if( !ismob(M) || !M.client ) return cmd_admin_pm(M.client,null) - feedback_add_details("admin_verb","APMM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + feedback_add_details("admin_verb","Admin PM Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! //shows a list of clients we could send PMs to, then forwards our choice to cmd_admin_pm /client/proc/cmd_admin_pm_panel() set category = "Admin" set name = "Admin PM" if(!holder) - src << "Error: Admin-PM-Panel: Only administrators may use this command." + to_chat(src, "Error: Admin-PM-Panel: Only administrators may use this command.") return var/list/client/targets[0] for(var/client/T) if(T.mob) - if(istype(T.mob, /mob/new_player)) + if(isnewplayer(T.mob)) targets["(New Player) - [T]"] = T - else if(istype(T.mob, /mob/observer/dead)) + else if(isobserver(T.mob)) targets["[T.mob.name](Ghost) - [T]"] = T else targets["[T.mob.real_name](as [T.mob.name]) - [T]"] = T else targets["(No Mob) - [T]"] = T - var/list/sorted = sortList(targets) - var/target = input(src,"To whom shall we send a message?","Admin PM",null) in sorted|null + var/target = input(src,"To whom shall we send a message?","Admin PM",null) as null|anything in sortList(targets) cmd_admin_pm(targets[target],null) - feedback_add_details("admin_verb","APM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + feedback_add_details("admin_verb","Admin PM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! +/client/proc/cmd_ahelp_reply(whom) + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, "Error: Admin-PM: You are unable to use admin PM-s (muted).") + return + var/client/C + if(istext(whom)) + if(cmptext(copytext(whom,1,2),"@")) + whom = findStealthKey(whom) + C = GLOB.directory[whom] + else if(istype(whom,/client)) + C = whom + if(!C) + if(holder) + to_chat(src, "Error: Admin-PM: Client not found.") + return + + var/datum/admin_help/AH = C.current_ticket + + if(AH) + message_admins("[key_name_admin(src)] has started replying to [key_name(C, 0, 0)]'s admin help.") + var/msg = input(src,"Message:", "Private message to [key_name(C, 0, 0)]") as text|null + if (!msg) + message_admins("[key_name_admin(src)] has cancelled their reply to [key_name(C, 0, 0)]'s admin help.") + return + cmd_admin_pm(whom, msg, AH) //takes input from cmd_admin_pm_context, cmd_admin_pm_panel or /client/Topic and sends them a PM. //Fetching a message if needed. src is the sender and C is the target client - -/client/proc/cmd_admin_pm(var/client/C, var/msg = null) +/client/proc/cmd_admin_pm(whom, msg, datum/admin_help/AH) if(prefs.muted & MUTE_ADMINHELP) - src << "Error: Private-Message: You are unable to use PM-s (muted)." + to_chat(src, "Error: Admin-PM: You are unable to use admin PM-s (muted).") return - if(!istype(C,/client)) - if(holder) src << "Error: Private-Message: Client not found." - else src << "Error: Private-Message: Client not found. They may have lost connection, so try using an adminhelp!" + if(!holder && !current_ticket) //no ticket? https://www.youtube.com/watch?v=iHSPf6x1Fdo + to_chat(src, "You can no longer reply to this ticket, please open another one by using the Adminhelp verb if need be.") + to_chat(src, "Message: [msg]") return - //get message text, limit it's length.and clean/escape html - if(!msg) - msg = input(src,"Message:", "Private message to [key_name(C, 0, holder ? 1 : 0)]") as text|null + var/client/recipient + var/irc = 0 + if(istext(whom)) + if(cmptext(copytext(whom,1,2),"@")) + whom = findStealthKey(whom) + if(whom == "IRCKEY") + irc = 1 + else + recipient = GLOB.directory[whom] + else if(istype(whom,/client)) + recipient = whom - if(!msg) return - if(!C) - if(holder) src << "Error: Admin-PM: Client not found." - else src << "Error: Private-Message: Client not found. They may have lost connection, so try using an adminhelp!" + + if(irc) + if(!ircreplyamount) //to prevent people from spamming irc + return + if(!msg) + msg = input(src,"Message:", "Private message to Administrator") as text|null + + if(!msg) + return + if(holder) + to_chat(src, "Error: Use the admin IRC channel, nerd.") return - //clean the message if it's not sent by a high-rank admin - //todo: sanitize for all??? - if(!check_rights(R_SERVER|R_DEBUG,0)) - msg = sanitize(msg) - if(!msg) return - var/recieve_pm_type = "Player" - if(holder) - //mod PMs are maroon - //PMs sent from admins and mods display their rank - if(holder) - if(!C.holder && holder && holder.fakekey) - recieve_pm_type = "Admin" + else + if(!recipient) + if(holder) + to_chat(src, "Error: Admin-PM: Client not found.") + to_chat(src, msg) else - recieve_pm_type = holder.rank + current_ticket.MessageNoRecipient(msg) + return - else if(!C.holder) - src << "Error: Admin-PM: Non-admin to non-admin PM communication is forbidden." - return + //get message text, limit it's length.and clean/escape html + if(!msg) + msg = input(src,"Message:", "Private message to [key_name(recipient, 0, 0)]") as text|null - var/recieve_message - - if(holder && !C.holder) - recieve_message = "-- Click the [recieve_pm_type]'s name to reply --\n" - if(C.adminhelped) - C << recieve_message - C.adminhelped = 0 - - //AdminPM popup for ApocStation and anybody else who wants to use it. Set it with POPUP_ADMIN_PM in config.txt ~Carn - if(config.popup_admin_pm) - spawn(0) //so we don't hold the caller proc up - var/sender = src - var/sendername = key - var/reply = sanitize(input(C, msg,"[recieve_pm_type] PM from [sendername]", "") as text|null) //show message and await a reply - if(C && reply) - if(sender) - C.cmd_admin_pm(sender,reply) //sender is still about, let's reply to them - else - adminhelp(reply) //sender has left, adminhelp instead + if(!msg) return - src << "" + create_text_tag("pm_out_alt", "PM", src) + " to [get_options_bar(C, holder ? 1 : 0, holder ? 1 : 0, 1)]: [msg]" - C << "" + create_text_tag("pm_in", "", C) + " \[[recieve_pm_type] PM\] [get_options_bar(src, C.holder ? 1 : 0, C.holder ? 1 : 0, 1)]: [msg]" - //play the recieving admin the adminhelp sound (if they have them enabled) - //non-admins shouldn't be able to disable this - if(C.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) - C << 'sound/effects/adminhelp.ogg' + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, "Error: Admin-PM: You are unable to use admin PM-s (muted).") + return - log_adminpm(msg,src,C) - send2adminirc("Reply: [key_name(src)]->[key_name(C)]: [html_decode(msg)]") + if(!recipient) + if(holder) + to_chat(src, "Error: Admin-PM: Client not found.") + else + current_ticket.MessageNoRecipient(msg) + return - //we don't use message_admins here because the sender/receiver might get it too - for(var/client/X in admins) - //check client/X is an admin and isn't the sender or recipient - if(X == C || X == src) - continue - if(X.key != key && X.key != C.key && (X.holder.rights & R_ADMIN|R_MOD|R_EVENT)) - X << "" + create_text_tag("pm_other", "PM:", X) + " [key_name(src, X, 0)] to [key_name(C, X, 0)]: [msg]" - -/client/proc/cmd_admin_irc_pm(sender) - if(prefs.muted & MUTE_ADMINHELP) - src << "Error: Private-Message: You are unable to use PM-s (muted)." + if (src.handle_spam_prevention(msg,MUTE_ADMINHELP)) return - var/msg = input(src,"Message:", "Reply private message to [sender] on IRC / 400 character limit") as text|null + //clean the message if it's not sent by a high-rank admin + if(!check_rights(R_SERVER|R_DEBUG,0)||irc)//no sending html to the poor bots + msg = trim(sanitize(copytext(msg,1,MAX_MESSAGE_LEN))) + if(!msg) + return + var/rawmsg = msg + + var/keywordparsedmsg = keywords_lookup(msg) + + if(irc) + to_chat(src, "PM to-Admins: [rawmsg]") + admin_ticket_log(src, "Reply PM from-[key_name(src, TRUE, TRUE)] to IRC: [keywordparsedmsg]") + ircreplyamount-- + send2irc("Reply: [ckey]",rawmsg) + else + if(recipient.holder) + if(holder) //both are admins + to_chat(recipient, "Admin PM from-[key_name(src, recipient, 1)]: [keywordparsedmsg]") + to_chat(src, "Admin PM to-[key_name(recipient, src, 1)]: [keywordparsedmsg]") + + //omg this is dumb, just fill in both their tickets + var/interaction_message = "PM from-[key_name(src, recipient, 1)] to-[key_name(recipient, src, 1)]: [keywordparsedmsg]" + admin_ticket_log(src, interaction_message) + if(recipient != src) //reeee + admin_ticket_log(recipient, interaction_message) + + else //recipient is an admin but sender is not + var/replymsg = "Reply PM from-[key_name(src, recipient, 1)]: [keywordparsedmsg]" + admin_ticket_log(src, replymsg) + to_chat(recipient, replymsg) + to_chat(src, "PM to-Admins: [msg]") + + //play the recieving admin the adminhelp sound (if they have them enabled) + if(recipient.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) + recipient << 'sound/effects/adminhelp.ogg' + + else + if(holder) //sender is an admin but recipient is not. Do BIG RED TEXT + if(!recipient.current_ticket) + new /datum/admin_help(msg, recipient, TRUE) + + to_chat(recipient, "-- Administrator private message --") + to_chat(recipient, "Admin PM from-[key_name(src, recipient, 0)]: [msg]") + to_chat(recipient, "Click on the administrator's name to reply.") + to_chat(src, "Admin PM to-[key_name(recipient, src, 1)]: [msg]") + + admin_ticket_log(recipient, "PM From [key_name_admin(src)]: [keywordparsedmsg]") + + //always play non-admin recipients the adminhelp sound + recipient << 'sound/effects/adminhelp.ogg' + + //AdminPM popup for ApocStation and anybody else who wants to use it. Set it with POPUP_ADMIN_PM in config.txt ~Carn + if(config.popup_admin_pm) + spawn() //so we don't hold the caller proc up + var/sender = src + var/sendername = key + var/reply = input(recipient, msg,"Admin PM from-[sendername]", "") as text|null //show message and await a reply + if(recipient && reply) + if(sender) + recipient.cmd_admin_pm(sender,reply) //sender is still about, let's reply to them + else + adminhelp(reply) //sender has left, adminhelp instead + return + + else //neither are admins + to_chat(src, "Error: Admin-PM: Non-admin to non-admin PM communication is forbidden.") + return + + if(irc) + log_admin("PM: [key_name(src)]->IRC: [rawmsg]") + for(var/client/X in GLOB.admins) + to_chat(X, "PM: [key_name(src, X, 0)]->IRC: [keywordparsedmsg]") + else + log_admin("PM: [key_name(src)]->[key_name(recipient)]: [rawmsg]") + //we don't use message_admins here because the sender/receiver might get it too + for(var/client/X in GLOB.admins) + if(X.key!=key && X.key!=recipient.key) //check client/X is an admin and isn't the sender or recipient + to_chat(X, "PM: [key_name(src, X, 0)]->[key_name(recipient, X, 0)]: [keywordparsedmsg]" ) + +/proc/IrcPm(target,msg,sender) + var/client/C = GLOB.directory[target] + + var/datum/admin_help/ticket = C ? C.current_ticket : GLOB.ahelp_tickets.CKey2ActiveTicket(target) + var/compliant_msg = trim(lowertext(msg)) + var/irc_tagged = "[sender](IRC)" + var/list/splits = splittext(compliant_msg, " ") + if(splits.len && splits[1] == "ticket") + if(splits.len < 2) + return "Usage: ticket " + switch(splits[2]) + if("close") + if(ticket) + ticket.Close(irc_tagged) + return "Ticket #[ticket.id] successfully closed" + if("resolve") + if(ticket) + ticket.Resolve(irc_tagged) + return "Ticket #[ticket.id] successfully resolved" + if("icissue") + if(ticket) + ticket.ICIssue(irc_tagged) + return "Ticket #[ticket.id] successfully marked as IC issue" + if("reject") + if(ticket) + ticket.Reject(irc_tagged) + return "Ticket #[ticket.id] successfully rejected" + else + return "Usage: ticket " + return "Error: Ticket could not be found" + + var/static/stealthkey + var/adminname = "Administrator" + + if(!C) + return "Error: No client" + + if(!stealthkey) + stealthkey = GenIrcStealthKey() + + msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN)) if(!msg) - return + return "Error: No message" - sanitize(msg) + message_admins("IRC message from [sender] to [key_name_admin(C)] : [msg]") + log_admin("IRC PM: [sender] -> [key_name(C)] : [msg]") - // Handled on Bot32's end, unsure about other bots -// if(length(msg) > 400) // TODO: if message length is over 400, divide it up into seperate messages, the message length restriction is based on IRC limitations. Probably easier to do this on the bots ends. -// src << "Your message was not sent because it was more then 400 characters find your message below for ease of copy/pasting" -// src << "[msg]" -// return + to_chat(C, "-- Administrator private message --") + to_chat(C, "Admin PM from-[adminname]: [msg]") + to_chat(C, "Click on the administrator's name to reply.") - send2adminirc("PlayerPM to [sender] from [key_name(src)]: [html_decode(msg)]") + admin_ticket_log(C, "PM From [irc_tagged]: [msg]") - src << "" + create_text_tag("pm_out_alt", "", src) + " to IRC-[sender]: [msg]" + window_flash(C, ignorepref = TRUE) + //always play non-admin recipients the adminhelp sound + C << 'sound/effects/adminhelp.ogg' - log_admin("PM: [key_name(src)]->IRC-[sender]: [msg]") - for(var/client/X in admins) - if(X == src) - continue - if(X.holder.rights & R_ADMIN|R_MOD) - X << "" + create_text_tag("pm_other", "PM:", X) + " [key_name(src, X, 0)] to IRC-[sender]: [msg]" + C.ircreplyamount = IRCREPLYCOUNT + + return "Message Successful" + +/proc/GenIrcStealthKey() + var/num = (rand(0,1000)) + var/i = 0 + while(i == 0) + i = 1 + for(var/P in GLOB.stealthminID) + if(num == GLOB.stealthminID[P]) + num++ + i = 0 + var/stealth = "@[num2text(num)]" + GLOB.stealthminID["IRCKEY"] = stealth + return stealth + +#undef IRCREPLYCOUNT \ No newline at end of file diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index 0373b8bf5d..20a187e84e 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -96,7 +96,9 @@ M << "You hear a voice in your head... [msg]" log_admin("SubtlePM: [key_name(usr)] -> [key_name(M)] : [msg]") - message_admins("SubtleMessage: [key_name_admin(usr)] -> [key_name_admin(M)] : [msg]", 1) + msg = " SubtleMessage: [key_name_admin(usr)] -> [key_name_admin(M)] : [msg]" + message_admins(msg) + admin_ticket_log(M, msg) feedback_add_details("admin_verb","SMS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/cmd_admin_world_narrate() // Allows administrators to fluff events a little easier -- TLE @@ -135,9 +137,11 @@ if( !msg ) return - M << msg + to_chat(M, msg) log_admin("DirectNarrate: [key_name(usr)] to ([M.name]/[M.key]): [msg]") - message_admins(" DirectNarrate: [key_name(usr)] to ([M.name]/[M.key]): [msg]
", 1) + msg = " DirectNarrate: [key_name(usr)] to ([M.name]/[M.key]): [msg]
" + message_admins(msg) + admin_ticket_log(M, msg) feedback_add_details("admin_verb","DIRN") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/cmd_admin_godmode(mob/M as mob in mob_list) @@ -150,7 +154,9 @@ usr << " Toggled [(M.status_flags & GODMODE) ? "ON" : "OFF"]" log_admin("[key_name(usr)] has toggled [key_name(M)]'s nodamage to [(M.status_flags & GODMODE) ? "On" : "Off"]") - message_admins("[key_name_admin(usr)] has toggled [key_name_admin(M)]'s nodamage to [(M.status_flags & GODMODE) ? "On" : "Off"]", 1) + var/msg = "[key_name_admin(usr)] has toggled [ADMIN_LOOKUPFLW(M)]'s nodamage to [(M.status_flags & GODMODE) ? "On" : "Off"]" + message_admins(msg) + admin_ticket_log(M, msg) feedback_add_details("admin_verb","GOD") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -540,7 +546,9 @@ Traitors and the like can also be revived with the previous role mostly intact. M.revive() log_admin("[key_name(usr)] healed / revived [key_name(M)]") - message_admins("Admin [key_name_admin(usr)] healed / revived [key_name_admin(M)]!", 1) + var/msg = "Admin [key_name_admin(usr)] healed / revived [ADMIN_LOOKUPFLW(M)]!" + message_admins(msg) + admin_ticket_log(M, msg) else alert("Admin revive disabled") feedback_add_details("admin_verb","REJU") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm index d1412a2a12..bf543230cc 100644 --- a/code/modules/client/client defines.dm +++ b/code/modules/client/client defines.dm @@ -12,6 +12,7 @@ var/last_message = "" //Contains the last message sent by this client - used to protect against copy-paste spamming. var/last_message_count = 0 //contins a number of how many times a message identical to last_message was sent. + var/ircreplyamount = 0 ///////// //OTHER// diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index 1202147210..d21dc16f48 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -57,7 +57,7 @@ if(mute_irc) usr << "" return - cmd_admin_irc_pm(href_list["irc_msg"]) + send2adminirc(href_list["irc_msg"]) return @@ -111,6 +111,8 @@ clients += src directory[ckey] = src + GLOB.ahelp_tickets.ClientLogin(src) + //Admin Authorisation holder = admin_datums[ckey] if(holder) @@ -176,6 +178,7 @@ if(holder) holder.owner = null admins -= src + GLOB.ahelp_tickets.ClientLogout(src) directory -= ckey clients -= src return ..() diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 30e966173e..107b164165 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -695,7 +695,10 @@ stat(null) for(var/datum/controller/subsystem/SS in Master.subsystems) SS.stat_entry() - + + if(statpanel("Tickets")) + GLOB.ahelp_tickets.stat_entry() + if(listed_turf && client) if(!TurfAdjacent(listed_turf)) listed_turf = null diff --git a/code/world.dm b/code/world.dm index bf5a43bb27..4936586033 100644 --- a/code/world.dm +++ b/code/world.dm @@ -62,6 +62,8 @@ var/global/datum/global_init/init = new () if(config && config.log_runtime) log = file("data/logs/runtime/[time2text(world.realtime,"YYYY-MM-DD-(hh-mm-ss)")]-runtime.log") + GLOB.timezoneOffset = text2num(time2text(0,"hh")) * 36000 + callHook("startup") //Emergency Fix load_mods() diff --git a/interface/interface.dm b/interface/interface.dm index 9adcfc965a..6ecfde4e7c 100644 --- a/interface/interface.dm +++ b/interface/interface.dm @@ -6,7 +6,7 @@ if(config.wikiurl) if(query) if(config.wikisearchurl) - var/output = replacetext(config.wikisearchurl, "%s", url_encode(query)) + var/output = replacetext(config.wikisearchurl, "%s", url_encode(query)) src << link(output) else src << " The wiki search URL is not set in the server configuration." diff --git a/polaris.dme b/polaris.dme index d28c7d40ce..9e10c638f5 100644 --- a/polaris.dme +++ b/polaris.dme @@ -55,6 +55,7 @@ #include "code\__defines\supply.dm" #include "code\__defines\targeting.dm" #include "code\__defines\turfs.dm" +#include "code\__defines\typeids.dm" #include "code\__defines\unit_tests.dm" #include "code\__defines\xenoarcheaology.dm" #include "code\__defines\ZAS.dm"