diff --git a/code/__defines/admin.dm b/code/__defines/admin.dm index 4c3aa6ddbd..cdf3cc1b60 100644 --- a/code/__defines/admin.dm +++ b/code/__defines/admin.dm @@ -41,10 +41,12 @@ #define R_MOD (1<<13) #define R_EVENT (1<<14) #define R_HOST (1<<15) //higher than this will overflow +#define R_MENTOR (1<<16) #define R_DEFAULT R_NONE -#define R_EVERYTHING (1<<16)-1 //the sum of all other rank permissions, used for +EVERYTHING +#define R_EVERYTHING (1<<17)-1 //the sum of all other rank permissions, used for +EVERYTHING +#define R_HOLDER ((R_EVERYTHING) & (~R_MENTOR)) #define SMITE_BREAKLEGS "Break Legs" #define SMITE_BLUESPACEARTILLERY "Bluespace Artillery" diff --git a/code/__defines/flags.dm b/code/__defines/flags.dm index 290fffe018..66a59bc96f 100644 --- a/code/__defines/flags.dm +++ b/code/__defines/flags.dm @@ -11,7 +11,7 @@ //check if all bitflags specified are present #define CHECK_MULTIPLE_BITFIELDS(flagvar, flags) ((flagvar & (flags)) == flags) -GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768)) +GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304)) /* Directions */ ///All the cardinal direction bitflags. diff --git a/code/__defines/span.dm b/code/__defines/span.dm index fb72e36468..430efef23e 100644 --- a/code/__defines/span.dm +++ b/code/__defines/span.dm @@ -204,8 +204,8 @@ // Mentor pm filter #define span_mentor(str) ("" + str + "") -#define span_mentor_pm_notice(str) ("" + str + "") -#define span_mentor_pm_warning(str) ("" + str + "") +#define span_mentor_notice(str) ("" + str + "") +#define span_mentor_warning(str) ("" + str + "") /* Adminchat */ // All of those have their own filter diff --git a/code/_global_vars/bitfields.dm b/code/_global_vars/bitfields.dm index b77364dcdb..9c39834eb5 100644 --- a/code/_global_vars/bitfields.dm +++ b/code/_global_vars/bitfields.dm @@ -42,6 +42,7 @@ DEFINE_BITFIELD(admin_flags, list( "SPAWN" = R_SPAWN, "STEALTH" = R_STEALTH, "VAREDIT" = R_VAREDIT, + "MENTOR" = R_MENTOR, )) DEFINE_BITFIELD(datum_flags, list( diff --git a/code/_global_vars/mobs.dm b/code/_global_vars/mobs.dm index 9e1027bc88..7e60dc71ea 100644 --- a/code/_global_vars/mobs.dm +++ b/code/_global_vars/mobs.dm @@ -1,7 +1,5 @@ GLOBAL_LIST_EMPTY(admins) //all clients whom are admins GLOBAL_PROTECT(admins) -GLOBAL_LIST_EMPTY(mentors) -GLOBAL_PROTECT(mentors) 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 diff --git a/code/_helpers/type2type.dm b/code/_helpers/type2type.dm index bfdbb12320..43aab83243 100644 --- a/code/_helpers/type2type.dm +++ b/code/_helpers/type2type.dm @@ -213,6 +213,7 @@ if (rights & R_SPAWN) . += "[seperator]+SPAWN" if (rights & R_MOD) . += "[seperator]+MODERATOR" if (rights & R_EVENT) . += "[seperator]+EVENT" + if (rights & R_MENTOR) . += "[seperator]+MENTOR" return . // Converts a hexadecimal color (e.g. #FF0050) to a list of numbers for red, green, and blue (e.g. list(255,0,80) ). diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm index 59d39fb22c..885c7ad5b2 100644 --- a/code/_onclick/hud/alert.dm +++ b/code/_onclick/hud/alert.dm @@ -2,9 +2,6 @@ //PUBLIC - call these wherever you want - -/mob/proc/throw_alert(category, type, severity, obj/new_master) - /** Proc to create or update an alert. Returns the alert if the alert is new or updated, 0 if it was thrown already * category is a text string. Each mob may only have one alert per category; the previous one will be replaced * path is a type path of the actual alert type to throw @@ -13,7 +10,7 @@ * new_master is optional and sets the alert's icon state to "template" in the ui_style icons with the master as an overlay. * Clicks are forwarded to master */ - +/mob/proc/throw_alert(category, type, severity, obj/new_master) if(!category) return @@ -428,6 +425,23 @@ so as to remain in compliance with the most up-to-date laws." if(isliving(usr)) var/mob/living/L = usr return L.resist() + +// TICKETS +/obj/screen/alert/open_ticket + icon = 'icons/logo.dmi' + name = "Admin Chat Request" + desc = "A Administrator would like to chat with you. \ + Click here to begin." + icon_state = "32x32" + +/obj/screen/alert/open_ticket/Click() + if(!usr || !usr.client) return + + // Open a new chat with the user + var/datum/ticket_chat/TC = new() + TC.T = usr.client.current_ticket + TC.tgui_interact(usr.client.mob) + // PRIVATE = only edit, use, or override these if you're editing the system as a whole // Re-render all alerts - also called in /datum/hud/show_hud() because it's needed there diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 4d148f21c9..480e300122 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -735,3 +735,14 @@ /datum/config_entry/flag/pixel_size_limit default = FALSE + +/// These are for tgs4 channels, for discord chatbots used in TGS. +/datum/config_entry/string/ahelp_channel_tag + +/// Turn this off if you don't want the TGS bot sending you messages whenever an ahelp ticket is created. +/datum/config_entry/flag/discord_ahelps_disabled + default = FALSE + +/// Turn this on if you want all admin-PMs to go to be sent to discord, and not only the first message of a ticket. +/datum/config_entry/flag/discord_ahelps_all + default = FALSE diff --git a/code/controllers/subsystems/statpanel.dm b/code/controllers/subsystems/statpanel.dm index 6bb5cd7fb6..50ae6e7957 100644 --- a/code/controllers/subsystems/statpanel.dm +++ b/code/controllers/subsystems/statpanel.dm @@ -63,7 +63,9 @@ SUBSYSTEM_DEF(statpanels) //target.stat_panel.send_message("update_split_admin_tabs", !!(target.prefs.toggles & SPLIT_ADMIN_TABS)) target.stat_panel.send_message("update_split_admin_tabs", FALSE) - if(!("MC" in target.panel_tabs) || !("Tickets" in target.panel_tabs)) + if(check_rights_for(target, R_MENTOR)) + target.stat_panel.send_message("add_tickets_tabs", target.holder.href_token) + else if(!("MC" in target.panel_tabs) || !("Tickets" in target.panel_tabs)) target.stat_panel.send_message("add_admin_tabs", target.holder.href_token) //if(target.stat_tab == "MC" && ((num_fires % mc_wait == 0) || target?.prefs.read_preference(/datum/preference/toggle/fast_mc_refresh))) @@ -178,9 +180,8 @@ SUBSYSTEM_DEF(statpanels) /datum/controller/subsystem/statpanels/proc/set_tickets_tab(client/target) var/list/tickets = list() - if(check_rights_for(target, R_ADMIN|R_SERVER|R_MOD)) //Prevents non-staff from opening the list of ahelp tickets - tickets += GLOB.ahelp_tickets.stat_entry(target) - tickets += GLOB.mhelp_tickets.stat_entry(target) + if(check_rights_for(target, R_ADMIN|R_SERVER|R_MOD|R_MENTOR)) //Prevents non-staff from opening the list of ahelp tickets + tickets = GLOB.tickets.stat_entry(target) target.stat_panel.send_message("update_tickets", tickets) /datum/controller/subsystem/statpanels/proc/set_SDQL2_tab(client/target) diff --git a/code/game/world.dm b/code/game/world.dm index 0a3dae6c3d..eb9c167fc7 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -494,7 +494,6 @@ var/world_topic_spam_protect_time = world.timeofday /hook/startup/proc/loadMods() world.load_mods() - world.load_mentors() // no need to write another hook. return 1 /world/proc/load_mods() @@ -518,40 +517,6 @@ var/world_topic_spam_protect_time = world.timeofday var/datum/admins/D = new /datum/admins(title, rights, ckey) D.associate(GLOB.directory[ckey]) -/world/proc/load_mentors() - if(CONFIG_GET(flag/admin_legacy_system)) - var/text = file2text("config/mentors.txt") - if (!text) - error("Failed to load config/mentors.txt") - else - var/list/lines = splittext(text, "\n") - for(var/line in lines) - if (!line) - continue - if (copytext(line, 1, 2) == ";") - continue - - var/ckey = copytext(line, 1, length(line)+1) - var/datum/mentor/M = new /datum/mentor(ckey) - M.associate(GLOB.directory[ckey]) - else - establish_db_connection() - if(!SSdbcore.IsConnected()) - error("Failed to connect to database in load_mentors().") - log_misc("Failed to connect to database in load_mentors().") - return - - var/datum/db_query/query = SSdbcore.NewQuery("SELECT ckey, mentor FROM erro_mentor") - query.Execute() - while(query.NextRow()) - var/ckey = query.item[1] - var/mentor = query.item[2] - - if(mentor) - var/datum/mentor/M = new /datum/mentor(ckey) - M.associate(GLOB.directory[ckey]) - qdel(query) - /world/proc/update_status() var/s = "" diff --git a/code/modules/admin/admin_ranks.dm b/code/modules/admin/admin_ranks.dm index e567ecb27d..2995552033 100644 --- a/code/modules/admin/admin_ranks.dm +++ b/code/modules/admin/admin_ranks.dm @@ -81,6 +81,8 @@ GLOBAL_PROTECT(protected_ranks) flag = R_MOD if("EVENT") flag = R_EVENT + if("MENTOR") + flag = R_MENTOR if("@") if(previous_rank) switch(group_count) diff --git a/code/modules/admin/admin_report.dm b/code/modules/admin/admin_report.dm deleted file mode 100644 index 2bc01ad38b..0000000000 --- a/code/modules/admin/admin_report.dm +++ /dev/null @@ -1,183 +0,0 @@ -// Reports are a way to notify admins of wrongdoings that happened -// while no admin was present. They work a bit similar to news, but -// they can only be read by admins and moderators. - -// a single admin report -/datum//admin_report/var - ID // the ID of the report - body // the content of the report - author // key of the author - date // date on which this was created - done // whether this was handled - - offender_key // store the key of the offender - offender_cid // store the cid of the offender - -/datum//report_topic_handler - Topic(href,href_list) - ..() - var/client/C = locate(href_list["client"]) - if(href_list["action"] == "show_reports") - C.display_admin_reports() - else if(href_list["action"] == "remove") - C.mark_report_done(text2num(href_list["ID"])) - else if(href_list["action"] == "edit") - C.edit_report(text2num(href_list["ID"])) - -var/datum/report_topic_handler/report_topic_handler - -world/New() - ..() - report_topic_handler = new - -// add a new news datums -/proc/make_report(body, author, okey, cid) - var/savefile/Reports = new("data/reports.sav") - var/list/reports - var/lastID - - Reports["reports"] >> reports - Reports["lastID"] >> lastID - - if(!reports) reports = list() - if(!lastID) lastID = 0 - - var/datum/admin_report/created = new() - created.ID = ++lastID - created.body = body - created.author = author - created.date = world.realtime - created.done = 0 - created.offender_key = okey - created.offender_cid = cid - - reports.Insert(1, created) - - Reports["reports"] << reports - Reports["lastID"] << lastID - -// load the reports from disk -/proc/load_reports() - var/savefile/Reports = new("data/reports.sav") - var/list/reports - - Reports["reports"] >> reports - - if(!reports) reports = list() - - return reports - -// check if there are any unhandled reports -/client/proc/unhandled_reports() - if(!src.holder) return 0 - var/list/reports = load_reports() - - for(var/datum/admin_report/N in reports) - if(N.done) - continue - else return 1 - - return 0 - -// checks if the player has an unhandled report against him -/client/proc/is_reported() - var/list/reports = load_reports() - - for(var/datum/admin_report/N in reports) if(!N.done) - if(N.offender_key == src.key) - return 1 - - return 0 - -// display only the reports that haven't been handled -/client/proc/display_admin_reports() - set category = "Admin.Moderation" - set name = "Display Admin Reports" - if(!src.holder) return - - var/list/reports = load_reports() - - var/output = "" - if(unhandled_reports()) - // load the list of unhandled reports - for(var/datum/admin_report/N in reports) - if(N.done) - continue - output += span_bold("Reported player:") + " [N.offender_key](CID: [N.offender_cid])
" - output += span_bold("Offense:") + "[N.body]
" - output += "Occurred at [time2text(N.date,"MM/DD hh:mm:ss")]
" - output += "authored by [N.author]
" - output += " Flag as Handled" - if(src.key == N.author) - output += " Edit" - output += "
" - output += "
" - else - output += "Whoops, no reports!" - - var/datum/browser/popup = new(src, "news", "News", 600, 400) - popup.set_content(output) - popup.open() - -/client/proc/Report(mob/M as mob in world) - set category = "Admin.Moderation" - if(!src.holder) - return - - var/CID = "Unknown" - if(M.client) - CID = M.client.computer_id - - var/body = tgui_input_text(src.mob, "Describe in detail what you're reporting [M] for", "Report") - if(!body) return - - - make_report(body, key, M.key, CID) - - spawn(1) - display_admin_reports() - -/client/proc/mark_report_done(ID as num) - if(!src.holder || src.holder.level < 0) - return - - var/savefile/Reports = new("data/reports.sav") - var/list/reports - - Reports["reports"] >> reports - - var/datum/admin_report/found - for(var/datum/admin_report/N in reports) - if(N.ID == ID) - found = N - if(!found) - to_chat(src, span_boldwarning("* An error occurred, sorry.")) - - found.done = 1 - - Reports["reports"] << reports - - -/client/proc/edit_report(ID as num) - if(!src.holder || src.holder.level < 0) - to_chat(src, span_boldwarning("You tried to modify the news, but you're not an admin!")) - return - - var/savefile/Reports = new("data/reports.sav") - var/list/reports - - Reports["reports"] >> reports - - var/datum/admin_report/found - for(var/datum/admin_report/N in reports) - if(N.ID == ID) - found = N - if(!found) - to_chat(src, span_boldwarning("* An error occurred, sorry.")) - - var/body = tgui_input_text(src.mob, "Enter a body for the news", "Body", multiline = TRUE, prevent_enter = TRUE) - if(!body) return - - found.body = body - - Reports["reports"] << reports diff --git a/code/modules/admin/admin_verb_lists_vr.dm b/code/modules/admin/admin_verb_lists_vr.dm index 492117b0d1..3bce37bf46 100644 --- a/code/modules/admin/admin_verb_lists_vr.dm +++ b/code/modules/admin/admin_verb_lists_vr.dm @@ -1,25 +1,4 @@ //admin verb groups - They can overlap if you so wish. Only one of each verb will exist in the verbs list regardless -var/list/admin_verbs_default = list( -// /datum/admins/proc/show_player_panel, //shows an interface for individual players, with various links (links require additional flags, //VOREStation Remove, -// /client/proc/player_panel_new, //shows an interface for all players, with links to various panels, //VOREStation Remove, -// /client/proc/player_panel, //VOREStation Remove, - /client/proc/cmd_admin_say, //VOREStation Add, - /client/proc/cmd_mod_say, //VOREStation Add, - /client/proc/cmd_event_say, //VOREStation Add, - /client/proc/cmd_mentor_ticket_panel, - /client/proc/cmd_mentor_say, -// /client/proc/hide_verbs, //hides all our adminverbs, //VOREStation Remove, -// /client/proc/hide_most_verbs, //hides all our hideable adminverbs, //VOREStation Remove, -// /client/proc/debug_variables, //allows us to -see- the variables of any instance in the game. +VAREDIT needed to modify, //VOREStation Remove, -// /client/proc/mark_datum_mapview, //VOREStation Remove, -// /client/proc/cmd_check_new_players, //allows us to see every new player, //VOREStation Remove, -// /client/proc/check_antagonists, //shows all antags, -// /client/proc/cmd_mod_say, -// /client/proc/deadchat //toggles deadchat on/off, -// /client/proc/toggle_ahelp_sound, - /client/proc/debugstatpanel, - ) - var/list/admin_verbs_admin = list( /client/proc/toggle_vantag_hud, /datum/admins/proc/set_tcrystals, @@ -83,9 +62,6 @@ var/list/admin_verbs_admin = list( /datum/admins/proc/toggleoocdead, //toggles ooc on/off for everyone who is dead, /datum/admins/proc/togglehubvisibility, //toggles visibility on the BYOND Hub., /datum/admins/proc/toggledsay, //toggles dsay on/off for everyone, - /client/proc/cmd_admin_say, //admin-only ooc chat, - /client/proc/cmd_mod_say, - /client/proc/cmd_event_say, /datum/admins/proc/PlayerNotes, /datum/admins/proc/show_player_info, /client/proc/free_slot, //frees slot for chosen job, @@ -117,8 +93,6 @@ var/list/admin_verbs_admin = list( /datum/admins/proc/sendFax, /client/proc/despawn_player, /datum/admins/proc/view_feedback, - /client/proc/make_mentor, - /client/proc/unmake_mentor, /client/proc/delbook, /client/proc/toggle_spawning_with_recolour, /client/proc/start_vote, @@ -199,7 +173,6 @@ var/list/admin_verbs_server = list( /datum/admins/proc/delay, /datum/admins/proc/toggleaban, /datum/admins/proc/togglepersistence, - /client/proc/cmd_mod_say, /client/proc/toggle_log_hrefs, /datum/admins/proc/immreboot, /client/proc/everyone_random, @@ -290,9 +263,6 @@ var/list/admin_verbs_possess = list( /proc/possess, /proc/release ) -var/list/admin_verbs_permissions = list( - /client/proc/edit_admin_permissions - ) var/list/admin_verbs_rejuv = list( /client/proc/respawn_character ) @@ -387,8 +357,6 @@ var/list/admin_verbs_mod = list( /client/proc/debug_variables, //allows us to -see- the variables of any instance in the game. +VAREDIT needed to modify, //VOREStation Add, /client/proc/mark_datum_mapview, //VOREStation Add, /client/proc/cmd_check_new_players, //allows us to see every new player, //VOREStation Add, - /client/proc/cmd_mod_say, - /client/proc/cmd_event_say, /datum/admins/proc/show_player_info, /datum/admins/proc/show_traitor_panel, /client/proc/player_panel_new, @@ -409,7 +377,6 @@ var/list/admin_verbs_mod = list( var/list/admin_verbs_event_manager = list( /client/proc/toggle_vantag_hud, - /client/proc/cmd_event_say, /client/proc/cmd_admin_pm_context, /client/proc/cmd_admin_pm_panel, /client/proc/admin_ghost, @@ -515,8 +482,6 @@ var/list/admin_verbs_event_manager = list( /client/proc/admin_memo, //admin memo system. show/delete/write. +SERVER needed to delete admin memos of others, /client/proc/dsay, //talk in deadchat using our ckey/fakekey, /client/proc/secrets, - /client/proc/cmd_mod_say, - /client/proc/cmd_event_say, /datum/admins/proc/show_player_info, /client/proc/free_slot, //frees slot for chosen job, /client/proc/cmd_admin_change_custom_event, @@ -543,7 +508,6 @@ var/list/admin_verbs_event_manager = list( /datum/admins/proc/startnow, /datum/admins/proc/restart, /datum/admins/proc/delay, - /client/proc/cmd_mod_say, /datum/admins/proc/immreboot, /client/proc/everyone_random, /client/proc/cmd_admin_delete, //delete an instance/object/mob/etc, diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 017da87b67..ae8b074a50 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -1,8 +1,7 @@ /client/proc/add_admin_verbs() // OLD ADMIN VERB SYSTEM - if(holder) - var/rights = holder.rank_flags() - add_verb(src, admin_verbs_default) + var/rights = holder.rank_flags() + if(rights & R_HOLDER) if(rights & R_BUILDMODE) add_verb(src, /client/proc/togglebuildmodeself) if(rights & R_ADMIN) add_verb(src, admin_verbs_admin) if(rights & R_FUN) add_verb(src, admin_verbs_fun) @@ -12,7 +11,6 @@ if(CONFIG_GET(flag/debugparanoid) && !(rights & R_ADMIN)) remove_verb(src, admin_verbs_paranoid_debug) //Right now it's just callproc but we can easily add others later on. if(rights & R_POSSESS) add_verb(src, admin_verbs_possess) - if(rights & R_PERMISSIONS) add_verb(src, admin_verbs_permissions) if(rights & R_STEALTH) add_verb(src, /client/proc/stealth) if(rights & R_REJUVINATE) add_verb(src, admin_verbs_rejuv) if(rights & R_SOUNDS) add_verb(src, admin_verbs_sounds) @@ -26,14 +24,12 @@ /client/proc/remove_admin_verbs() // OLD ADMIN VERB SYSTEM remove_verb(src, list( - admin_verbs_default, /client/proc/togglebuildmodeself, admin_verbs_admin, admin_verbs_fun, admin_verbs_server, admin_verbs_debug, admin_verbs_possess, - admin_verbs_permissions, /client/proc/stealth, admin_verbs_rejuv, admin_verbs_sounds, @@ -130,7 +126,7 @@ set category = "Admin.Game" set desc = "Toggles ghost-like invisibility (Don't abuse this)" - if(holder && mob) + if(check_rights(R_HOLDER) && mob) if(mob.invisibility > INVISIBILITY_OBSERVER) to_chat(mob, span_warning("You can't use this, your current invisibility level ([mob.invisibility]) is above the observer level ([INVISIBILITY_OBSERVER]).")) return @@ -149,7 +145,7 @@ /client/proc/player_panel() set name = "Player Panel" set category = "Admin.Game" - if(holder) + if(check_rights(R_HOLDER)) holder.player_panel_old() feedback_add_details("admin_verb","PP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! return @@ -157,7 +153,7 @@ /client/proc/player_panel_new() set name = "Player Panel New" set category = "Admin.Game" - if(holder) + if(check_rights(R_HOLDER)) holder.player_panel_new() feedback_add_details("admin_verb","PPN") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! return @@ -165,7 +161,7 @@ /client/proc/check_antagonists() set name = "Check Antagonists" set category = "Admin.Investigate" - if(holder) + if(check_rights(R_HOLDER)) holder.check_antagonists() log_admin("[key_name(usr)] checked antagonists.") //for tsar~ feedback_add_details("admin_verb","CHA") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -192,7 +188,7 @@ ADMIN_VERB(game_panel, R_ADMIN|R_SERVER|R_FUN, "Game Panel", "Look at the state /client/proc/secrets() set name = "Secrets" set category = "Admin.Secrets" - if (holder) + if(check_rights(R_HOLDER)) holder.Secrets() feedback_add_details("admin_verb","S") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! return @@ -219,7 +215,7 @@ ADMIN_VERB(game_panel, R_ADMIN|R_SERVER|R_FUN, "Game Panel", "Look at the state /client/proc/stealth() set category = "Admin.Game" set name = "Stealth Mode" - if(holder) + if(check_rights(R_HOLDER)) if(holder.fakekey) holder.fakekey = null if(isnewplayer(src.mob)) @@ -396,7 +392,7 @@ ADMIN_VERB(deadmin, R_NONE, "DeAdmin", "Shed your admin powers.", ADMIN_CATEGORY /client/proc/check_ai_laws() set name = "Check AI Laws" set category = "Admin.Silicon" - if(holder) + if(check_rights(R_HOLDER)) src.holder.output_ai_laws() /client/proc/rename_silicon() @@ -458,7 +454,7 @@ ADMIN_VERB(deadmin, R_NONE, "DeAdmin", "Shed your admin powers.", ADMIN_CATEGORY /client/proc/mod_panel() set name = "Moderator Panel" set category = "Admin.Moderation" -/* if(holder) +/* if(check_rights(R_HOLDER)) holder.mod_panel()*/ // feedback_add_details("admin_verb","MP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! return @@ -466,14 +462,14 @@ ADMIN_VERB(deadmin, R_NONE, "DeAdmin", "Shed your admin powers.", ADMIN_CATEGORY /client/proc/playernotes() set name = "Show Player Info" set category = "Admin.Moderation" - if(holder) + if(check_rights(R_HOLDER)) holder.PlayerNotes() return /client/proc/free_slot() set name = "Free Job Slot" set category = "Admin.Game" - if(holder) + if(check_rights(R_HOLDER)) var/list/jobs = list() for (var/datum/job/J in job_master.occupations) if (J.current_positions >= J.total_positions && J.total_positions != -1) @@ -542,11 +538,8 @@ ADMIN_VERB(deadmin, R_NONE, "DeAdmin", "Shed your admin powers.", ADMIN_CATEGORY log_admin("[key_name(usr)] gave [key_name(T)] the spell [S].") message_admins(span_blue("[key_name_admin(usr)] gave [key_name(T)] the spell [S]."), 1) -/client/proc/debugstatpanel() - set name = "Debug Stat Panel" - set category = "Debug.Misc" - - src.stat_panel.send_message("create_debug") +ADMIN_VERB(debugstatpanel, R_DEBUG, "Debug Stat Panel", "Allows to debug the statpanel", "Debug.Misc") + user.stat_panel.send_message("create_debug") /client/proc/spawn_reagent() set name = "Spawn Reagent" @@ -618,7 +611,7 @@ ADMIN_VERB(deadmin, R_NONE, "DeAdmin", "Shed your admin powers.", ADMIN_CATEGORY var/atom/movable/orbiter var/input - if(holder.marked_datum) + if(check_rights(R_HOLDER) && holder.marked_datum) input = tgui_alert(usr, "You have \n[holder.marked_datum] marked, should this be the center of the orbit, or the orbiter?", "Orbit", list("Center", "Orbiter", "Neither")) switch(input) if("Center") diff --git a/code/modules/admin/permissionedit.dm b/code/modules/admin/permissionedit.dm index 6dd91e1e40..ef61823dc7 100644 --- a/code/modules/admin/permissionedit.dm +++ b/code/modules/admin/permissionedit.dm @@ -1,12 +1,5 @@ -/client/proc/edit_admin_permissions() - set category = "Admin.Secrets" - set name = "Permissions Panel" - set desc = "Edit admin permissions" - - if(!check_rights(R_PERMISSIONS)) - return - - usr.client.holder.edit_admin_permissions() +ADMIN_VERB(edit_admin_permissions, R_PERMISSIONS, "Permissions Panel", "Edit admin permissions.", "Admin.Secrets") + user.holder.edit_admin_permissions() /datum/admins/proc/edit_admin_permissions(action, target, operation, page) if(!check_rights(R_PERMISSIONS)) diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index ec9b48ac7f..1b6a87b360 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -29,21 +29,21 @@ check_antagonists() return - if(href_list["ahelp"]) - if(!check_rights(R_ADMIN|R_MOD|R_DEBUG|R_EVENT)) + if(href_list["ticket"]) + if(!check_rights(R_ADMIN|R_MOD|R_DEBUG|R_EVENT|R_MENTOR)) return - var/ahelp_ref = href_list["ahelp"] - var/datum/admin_help/AH = locate(ahelp_ref) - if(AH) - AH.Action(href_list["ahelp_action"]) + var/ticket_ref = href_list["ticket"] + var/datum/ticket/T = locate(ticket_ref) + if(T) + T.Action(href_list["ticket_action"]) else - to_chat(usr, "Ticket [ahelp_ref] has been deleted!") + to_chat(usr, "Ticket [ticket_ref] has been deleted!") - else if(href_list["ahelp_tickets"]) - GLOB.ahelp_tickets.BrowseTickets(text2num(href_list["ahelp_tickets"])) + else if(href_list["tickets"]) + GLOB.tickets.BrowseTickets(text2num(href_list["tickets"])) - mentor_commands(href, href_list, src) + // mentor_commands(href, href_list, src) - Skip because client is already admin & contents handled above if(href_list["dbsearchckey"] || href_list["dbsearchadmin"]) @@ -865,9 +865,9 @@ to_chat(M, span_filter_system(span_warning("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(span_blue("[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() + var/datum/ticket/T = M.client ? M.client.current_ticket : null + if(T) + T.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") @@ -893,9 +893,9 @@ message_admins(span_blue("[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() + var/datum/ticket/T = M.client ? M.client.current_ticket : null + if(T) + T.Resolve() qdel(M.client) //qdel(M) if("Cancel") diff --git a/code/modules/admin/verbs/adminhelp_vr.dm b/code/modules/admin/verbs/adminhelp_vr.dm deleted file mode 100644 index eb02d4aa0a..0000000000 --- a/code/modules/admin/verbs/adminhelp_vr.dm +++ /dev/null @@ -1,39 +0,0 @@ -/datum/admin_help/proc/send2adminchat() - if(!CONFIG_GET(string/chat_webhook_url)) - return - - var/list/adm = get_admin_counts() - var/list/afkmins = adm["afk"] - var/list/allmins = adm["total"] - - spawn(0) //Unreliable world.Exports() - var/query_string = "type=adminhelp" - query_string += "&key=[url_encode(CONFIG_GET(string/chat_webhook_key))]" - query_string += "&from=[url_encode(key_name(initiator))]" - query_string += "&msg=[url_encode(html_decode(name))]" - query_string += "&admin_number=[allmins.len]" - query_string += "&admin_number_afk=[afkmins.len]" - world.Export("[CONFIG_GET(string/chat_webhook_url)]?[query_string]") - -/client/verb/adminspice() - set category = "Admin" - set name = "Request Spice" - set desc = "Request admins to spice round up for you" - - //handle muting and automuting - if(prefs.muted & MUTE_ADMINHELP) - to_chat(usr, span_danger("Error: You cannot request spice (muted from adminhelps).")) - return - - if(tgui_alert(usr, "Are you sure you want to request the admins spice things up for you? You accept the consequences if you do.","Spicy!",list("Yes","No")) == "Yes") - message_admins("[ADMIN_FULLMONTY(usr)] has requested the round be spiced up a little.") - to_chat(usr, span_notice("You have requested some more spice in your round.")) - else - to_chat(usr, span_notice("Spice request cancelled.")) - return - - //if they requested spice, then remove spice verb temporarily to prevent spamming - remove_verb(usr, /client/verb/adminspice) - spawn(10 MINUTES) - if(usr) // In case we left in the 10 minute cooldown - add_verb(usr, /client/verb/adminspice) // 10 minute cool-down for spice request diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index 367f3b9b5a..4d25f34d3a 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -53,19 +53,19 @@ to_chat(src, span_admin_pm_warning("Error: Admin-PM: Client not found.")) return - var/datum/admin_help/AH = C.current_ticket + var/datum/ticket/T = C.current_ticket - if(AH) + if(T) message_admins(span_pm("[key_name_admin(src)] has started replying to [key_name(C, 0, 0)]'s admin help.")) var/msg = tgui_input_text(src,"Message:", "Private message to [key_name(C, 0, 0)]", multiline = TRUE) if (!msg) message_admins(span_pm("[key_name_admin(src)] has cancelled their reply to [key_name(C, 0, 0)]'s admin help.")) return - cmd_admin_pm(whom, msg, AH) + cmd_admin_pm(whom, msg, T) //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(whom, msg, datum/admin_help/AH) +/client/proc/cmd_admin_pm(whom, msg, datum/ticket/T) if(prefs.muted & MUTE_ADMINHELP) to_chat(src, span_admin_pm_warning("Error: Admin-PM: You are unable to use admin PM-s (muted).")) return @@ -171,7 +171,7 @@ 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) + new /datum/ticket(msg, recipient, TRUE, 1) to_chat(recipient, span_admin_pm_warning(span_huge(span_bold("-- Administrator private message --")))) to_chat(recipient, span_admin_pm_warning("Admin PM from-[key_name(src, recipient, 0)]: [msg]")) @@ -218,7 +218,7 @@ /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/datum/ticket/ticket = C ? C.current_ticket : GLOB.tickets.CKey2ActiveTicket(target) var/compliant_msg = trim(lowertext(msg)) var/irc_tagged = "[sender](IRC)" var/list/splits = splittext(compliant_msg, " ") diff --git a/code/modules/admin/verbs/adminsay.dm b/code/modules/admin/verbs/adminsay.dm index 60a6080662..af647db526 100644 --- a/code/modules/admin/verbs/adminsay.dm +++ b/code/modules/admin/verbs/adminsay.dm @@ -1,63 +1,53 @@ -/client/proc/cmd_admin_say(msg as text) - set category = "Admin.Chat" - set name = "Asay" //Gave this shit a shorter name so you only have to time out "asay" rather than "admin say" to use it --NeoFite - set hidden = 1 - if(!check_rights(R_ADMIN)) //VOREStation Edit - return - - msg = sanitize(msg) +ADMIN_VERB(cmd_admin_say, R_ADMIN, "ASay", "Send a message to other admins", "Admin.Chat", message as text) + var/msg = sanitize(message) if(!msg) return - log_adminsay(msg,src) + log_adminsay(msg, user) for(var/client/C in GLOB.admins) if(check_rights_for(C, R_ADMIN)) - to_chat(C, span_admin_channel(create_text_tag("admin", "ADMIN:", C) + " " + span_name("[key_name(usr, 1)]") + "([admin_jump_link(mob, src)]): " + span_name("[msg]") )) + to_chat(C, span_admin_channel(create_text_tag("admin", "ADMIN:", C) + " " + span_name("[key_name(user, 1)]") + "([admin_jump_link(user.mob, C.holder)]): " + span_name("[msg]") )) feedback_add_details("admin_verb","M") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/cmd_mod_say(msg as text) - set category = "Admin.Chat" - set name = "Msay" - set hidden = 1 - - if(!check_rights(R_ADMIN|R_MOD|R_SERVER)) //VOREStation Edit - return - - msg = sanitize(msg) - log_modsay(msg,src) +ADMIN_VERB(cmd_mod_say, (R_ADMIN|R_MOD|R_SERVER), "Msay", "Send a message to other mod", "Admin.Chat", message as text) + var/msg = sanitize(message) + log_modsay(msg, user) if (!msg) return - var/sender_name = key_name(usr, 1) + var/sender_name = key_name(user, 1) if(check_rights(R_ADMIN, 0)) sender_name = span_admin("[sender_name]") for(var/client/C in GLOB.admins) if(check_rights(R_ADMIN|R_MOD|R_SERVER)) //VOREStation Edit - to_chat(C, span_mod_channel(create_text_tag("mod", "MOD:", C) + " " + span_name("[sender_name]") + "([admin_jump_link(mob, C.holder)]): " + span_name("[msg]") )) + to_chat(C, span_mod_channel(create_text_tag("mod", "MOD:", C) + " " + span_name("[sender_name]") + "([admin_jump_link(user.mob, C.holder)]): " + span_name("[msg]") )) feedback_add_details("admin_verb","MS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/cmd_event_say(msg as text) - set category = "Admin.Chat" - set name = "Esay" - set hidden = 1 - - if(!check_rights(R_ADMIN|R_MOD|R_EVENT|R_SERVER)) //VOREStation Edit - return - - msg = sanitize(msg) - log_eventsay(msg,src) +ADMIN_VERB(cmd_event_say, (R_ADMIN|R_MOD|R_EVENT|R_SERVER), "Esay", "Send a message to other event manager", "Admin.Chat", message as text) + var/msg = sanitize(message) + log_eventsay(msg, user) if (!msg) return - var/sender_name = key_name(usr, 1) + var/sender_name = key_name(user, 1) if(check_rights(R_ADMIN, 0)) sender_name = span_admin("[sender_name]") for(var/client/C in GLOB.admins) - to_chat(C, span_event_channel(create_text_tag("event", "EVENT:", C) + " " + span_name("[sender_name]") + "([admin_jump_link(mob, C.holder)]): " + span_name("[msg]") )) + to_chat(C, span_event_channel(create_text_tag("event", "EVENT:", C) + " " + span_name("[sender_name]") + "([admin_jump_link(user.mob, C.holder)]): " + span_name("[msg]") )) feedback_add_details("admin_verb","GS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +ADMIN_VERB(cmd_mentor_say, (R_ADMIN|R_MOD|R_EVENT|R_SERVER|R_MENTOR), "Mentorsay", "Send a message to other mentors", "Admin.Chat", message as text) + var/msg = sanitize(message) + if (!msg) + return + + log_admin("Mentorsay: [key_name(user)]: [msg]") + + for(var/client/C in GLOB.admins) + to_chat(C, create_text_tag("mentor", "MENTOR:", C) + " " + span_mentor_channel(span_name("[user]") + ": " + span_message("[msg]"))) diff --git a/code/modules/admin/view_variables/topic.dm b/code/modules/admin/view_variables/topic.dm index c237d12f32..b741b3946d 100644 --- a/code/modules/admin/view_variables/topic.dm +++ b/code/modules/admin/view_variables/topic.dm @@ -1,7 +1,7 @@ //DO NOT ADD MORE TO THIS FILE. //Use vv_do_topic()! /client/proc/view_var_Topic(href, href_list, hsrc) - if((usr.client != src) || !src.holder) + if((usr.client != src) || check_rights(R_HOLDER)) return var/datum/target = locate(href_list["target"]) if(istype(target)) @@ -18,10 +18,10 @@ var/mob/M = locate(href_list["rename"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") + to_chat(src, "This can only be used on instances of type /mob") return - var/new_name = sanitize(tgui_input_text(usr,"What would you like to name this mob?","Input a name",M.real_name,MAX_NAME_LEN), MAX_NAME_LEN) + var/new_name = sanitize(tgui_input_text(src,"What would you like to name this mob?","Input a name",M.real_name,MAX_NAME_LEN), MAX_NAME_LEN) if( !new_name || !M ) return message_admins("Admin [key_name_admin(usr)] renamed [key_name_admin(M)] to [new_name].") @@ -33,7 +33,7 @@ var/D = locate(href_list["datumedit"]) if(!istype(D,/datum) && !istype(D,/client)) - to_chat(usr, "This can only be used on instances of types /client or /datum") + to_chat(src, "This can only be used on instances of types /client or /datum") return modify_variables(D, href_list["varnameedit"], 1) @@ -43,7 +43,7 @@ var/D = locate(href_list["datumchange"]) if(!istype(D,/datum) && !istype(D,/client)) - to_chat(usr, "This can only be used on instances of types /client or /datum") + to_chat(src, "This can only be used on instances of types /client or /datum") return modify_variables(D, href_list["varnamechange"], 0) @@ -53,7 +53,7 @@ var/atom/A = locate(href_list["datummass"]) if(!istype(A)) - to_chat(usr, "This can only be used on instances of type /atom") + to_chat(src, "This can only be used on instances of type /atom") return cmd_mass_modify_object_variables(A, href_list["varnamemass"]) @@ -63,7 +63,7 @@ var/mob/M = locate(href_list["mob_player_panel"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") + to_chat(src, "This can only be used on instances of type /mob") return src.holder.show_player_panel(M) @@ -74,7 +74,7 @@ var/mob/M = locate(href_list["give_spell"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") + to_chat(src, "This can only be used on instances of type /mob") return src.give_spell(M) @@ -86,7 +86,7 @@ var/mob/living/M = locate(href_list["give_modifier"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob/living") + to_chat(src, "This can only be used on instances of type /mob/living") return src.admin_give_modifier(M) @@ -98,22 +98,22 @@ var/mob/living/carbon/human/H = locate(href_list["give_wound_internal"]) if(!istype(H)) - to_chat(usr, span_notice("This can only be used on instances of type /mob/living/carbon/human")) + to_chat(src, span_notice("This can only be used on instances of type /mob/living/carbon/human")) return - var/severity = tgui_input_number(usr, "How much damage should the bleeding internal wound cause? \ + var/severity = tgui_input_number(src, "How much damage should the bleeding internal wound cause? \ Bleed timer directly correlates with this. 0 cancels. Input is rounded to nearest integer.", "Wound Severity", 0) if(!severity) return - var/obj/item/organ/external/chosen_organ = tgui_input_list(usr, "Choose an external organ to inflict IB on!", "Organ Choice", H.organs) + var/obj/item/organ/external/chosen_organ = tgui_input_list(src, "Choose an external organ to inflict IB on!", "Organ Choice", H.organs) if(!chosen_organ || !istype(chosen_organ)) to_chat(usr, span_notice("The chosen organ is of inappropriate type or no longer exists.")) return var/datum/wound/internal_bleeding/I = new /datum/wound/internal_bleeding(severity) if(!I || !istype(I)) - to_chat(usr, span_notice("Could not initialize internal wound")) + to_chat(src, span_notice("Could not initialize internal wound")) log_debug("[usr] attempted to create an internal bleeding wound on [H]'s [chosen_organ] of [severity] damage \ and wound initialization failed") @@ -134,7 +134,7 @@ var/mob/M = locate(href_list["godmode"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") + to_chat(src, "This can only be used on instances of type /mob") return src.cmd_admin_godmode(M) @@ -145,7 +145,7 @@ var/mob/M = locate(href_list["gib"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") + to_chat(src, "This can only be used on instances of type /mob") return src.cmd_admin_gib(M) @@ -155,7 +155,7 @@ var/mob/M = locate(href_list["build_mode"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") + to_chat(src, "This can only be used on instances of type /mob") return togglebuildmode(M) @@ -166,7 +166,7 @@ var/mob/M = locate(href_list["drop_everything"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") + to_chat(src, "This can only be used on instances of type /mob") return if(usr.client) @@ -177,7 +177,7 @@ var/mob/M = locate(href_list["direct_control"]) if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") + to_chat(src, "This can only be used on instances of type /mob") return if(usr.client) @@ -188,22 +188,22 @@ var/mob/M = locate(href_list["give_ai"]) if(!isliving(M)) - to_chat(usr, span_notice("This can only be used on instances of type /mob/living")) + to_chat(src, span_notice("This can only be used on instances of type /mob/living")) return var/mob/living/L = M if(L.client || L.teleop) - to_chat(usr, span_warning("This cannot be used on player mobs!")) + to_chat(src, span_warning("This cannot be used on player mobs!")) return if(L.ai_holder) //Cleaning up the original ai var/ai_holder_old = L.ai_holder L.ai_holder = null qdel(ai_holder_old) //Only way I could make #TESTING - Unable to be GC'd to stop. del() logs show it works. - L.ai_holder_type = tgui_input_list(usr, "Choose AI holder", "AI Type", typesof(/datum/ai_holder/)) + L.ai_holder_type = tgui_input_list(src, "Choose AI holder", "AI Type", typesof(/datum/ai_holder/)) L.initialize_ai_holder() - L.faction = sanitize(tgui_input_text(usr, "Please input AI faction", "AI faction", "neutral")) - L.a_intent = tgui_input_list(usr, "Please choose AI intent", "AI intent", list(I_HURT, I_HELP)) - if(tgui_alert(usr, "Make mob wake up? This is needed for carbon mobs.", "Wake mob?", list("Yes", "No")) == "Yes") + L.faction = sanitize(tgui_input_text(src, "Please input AI faction", "AI faction", "neutral")) + L.a_intent = tgui_input_list(src, "Please choose AI intent", "AI intent", list(I_HURT, I_HELP)) + if(tgui_alert(src, "Make mob wake up? This is needed for carbon mobs.", "Wake mob?", list("Yes", "No")) == "Yes") L.AdjustSleeping(-100) else if(href_list["make_skeleton"]) @@ -211,7 +211,7 @@ var/mob/living/carbon/human/H = locate(href_list["make_skeleton"]) if(!istype(H)) - to_chat(usr, "This can only be used on instances of type /mob/living/carbon/human") + to_chat(src, "This can only be used on instances of type /mob/living/carbon/human") return H.ChangeToSkeleton() @@ -223,17 +223,17 @@ var/obj/O = locate(href_list["delall"]) if(!isobj(O)) - to_chat(usr, "This can only be used on instances of type /obj") + to_chat(src, "This can only be used on instances of type /obj") return - var/action_type = tgui_alert(usr, "Strict type ([O.type]) or type and all subtypes?","Type Selection",list("Strict type","Type and subtypes","Cancel")) + var/action_type = tgui_alert(src, "Strict type ([O.type]) or type and all subtypes?","Type Selection",list("Strict type","Type and subtypes","Cancel")) if(action_type == "Cancel" || !action_type) return - if(tgui_alert(usr, "Are you really sure you want to delete all objects of type [O.type]?","Delete All?",list("Yes","No")) != "Yes") + if(tgui_alert(src, "Are you really sure you want to delete all objects of type [O.type]?","Delete All?",list("Yes","No")) != "Yes") return - if(tgui_alert(usr, "Second confirmation required. Delete?","REALLY?",list("Yes","No")) != "Yes") + if(tgui_alert(src, "Second confirmation required. Delete?","REALLY?",list("Yes","No")) != "Yes") return var/O_type = O.type @@ -246,7 +246,7 @@ qdel(Obj) CHECK_TICK if(!i) - to_chat(usr, "No objects of this type exist") + to_chat(src, "No objects of this type exist") return log_admin("[key_name(usr)] deleted all objects of type [O_type] ([i] objects deleted) ") message_admins(span_notice("[key_name(usr)] deleted all objects of type [O_type] ([i] objects deleted) ")) @@ -258,7 +258,7 @@ qdel(Obj) CHECK_TICK if(!i) - to_chat(usr, "No objects of this type exist") + to_chat(src, "No objects of this type exist") return log_admin("[key_name(usr)] deleted all objects of type or subtype of [O_type] ([i] objects deleted) ") message_admins(span_notice("[key_name(usr)] deleted all objects of type or subtype of [O_type] ([i] objects deleted) ")) @@ -267,7 +267,7 @@ var/obj/item/pda/P = locate(href_list["fakepdapropconvo"]) if(!istype(P)) - to_chat(usr, span_warning("This can only be done to instances of type /pda")) + to_chat(src, span_warning("This can only be done to instances of type /pda")) return P.createPropFakeConversation_admin(usr) @@ -277,7 +277,7 @@ var/atom/A = locate(href_list["rotatedatum"]) if(!istype(A)) - to_chat(usr, "This can only be done to instances of type /atom") + to_chat(src, "This can only be done to instances of type /atom") return switch(href_list["rotatedir"]) @@ -290,12 +290,12 @@ var/mob/living/carbon/human/H = locate(href_list["makemonkey"]) if(!istype(H)) - to_chat(usr, "This can only be done to instances of type /mob/living/carbon/human") + to_chat(src, "This can only be done to instances of type /mob/living/carbon/human") return - if(tgui_alert(usr, "Confirm mob type change?","Confirm",list("Transform","Cancel")) != "Transform") return + if(tgui_alert(src, "Confirm mob type change?","Confirm",list("Transform","Cancel")) != "Transform") return if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return holder.Topic(href, list("monkeyone"=href_list["makemonkey"])) @@ -304,12 +304,12 @@ var/mob/living/carbon/human/H = locate(href_list["makerobot"]) if(!istype(H)) - to_chat(usr, "This can only be done to instances of type /mob/living/carbon/human") + to_chat(src, "This can only be done to instances of type /mob/living/carbon/human") return - if(tgui_alert(usr, "Confirm mob type change?","Confirm",list("Transform","Cancel")) != "Transform") return + if(tgui_alert(src, "Confirm mob type change?","Confirm",list("Transform","Cancel")) != "Transform") return if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return holder.Topic(href, list("makerobot"=href_list["makerobot"])) @@ -318,12 +318,12 @@ var/mob/living/carbon/human/H = locate(href_list["makealien"]) if(!istype(H)) - to_chat(usr, "This can only be done to instances of type /mob/living/carbon/human") + to_chat(src, "This can only be done to instances of type /mob/living/carbon/human") return - if(tgui_alert(usr, "Confirm mob type change?","Confirm",list("Transform","Cancel")) != "Transform") return + if(tgui_alert(src, "Confirm mob type change?","Confirm",list("Transform","Cancel")) != "Transform") return if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return holder.Topic(href, list("makealien"=href_list["makealien"])) @@ -332,12 +332,12 @@ var/mob/living/carbon/human/H = locate(href_list["makeai"]) if(!istype(H)) - to_chat(usr, "This can only be done to instances of type /mob/living/carbon/human") + to_chat(src, "This can only be done to instances of type /mob/living/carbon/human") return - if(tgui_alert(usr, "Confirm mob type change?","Confirm",list("Transform","Cancel")) != "Transform") return + if(tgui_alert(src, "Confirm mob type change?","Confirm",list("Transform","Cancel")) != "Transform") return if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return holder.Topic(href, list("makeai"=href_list["makeai"])) @@ -346,67 +346,67 @@ var/mob/living/carbon/human/H = locate(href_list["setspecies"]) if(!istype(H)) - to_chat(usr, "This can only be done to instances of type /mob/living/carbon/human") + to_chat(src, "This can only be done to instances of type /mob/living/carbon/human") return - var/new_species = tgui_input_list(usr, "Please choose a new species.","Species", GLOB.all_species) + var/new_species = tgui_input_list(src, "Please choose a new species.","Species", GLOB.all_species) if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return if(H.set_species(new_species)) - to_chat(usr, "Set species of [H] to [H.species].") + to_chat(src, "Set species of [H] to [H.species].") else - to_chat(usr, "Failed! Something went wrong.") + to_chat(src, "Failed! Something went wrong.") else if(href_list["addlanguage"]) if(!check_rights(R_SPAWN)) return var/mob/H = locate(href_list["addlanguage"]) if(!istype(H)) - to_chat(usr, "This can only be done to instances of type /mob") + to_chat(src, "This can only be done to instances of type /mob") return - var/new_language = tgui_input_list(usr, "Please choose a language to add.","Language", GLOB.all_languages) + var/new_language = tgui_input_list(src, "Please choose a language to add.","Language", GLOB.all_languages) if(!new_language) return if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return if(H.add_language(new_language)) - to_chat(usr, "Added [new_language] to [H].") + to_chat(src, "Added [new_language] to [H].") else - to_chat(usr, "Mob already knows that language.") + to_chat(src, "Mob already knows that language.") else if(href_list["remlanguage"]) if(!check_rights(R_SPAWN)) return var/mob/H = locate(href_list["remlanguage"]) if(!istype(H)) - to_chat(usr, "This can only be done to instances of type /mob") + to_chat(src, "This can only be done to instances of type /mob") return if(!H.languages.len) - to_chat(usr, "This mob knows no languages.") + to_chat(src, "This mob knows no languages.") return - var/datum/language/rem_language = tgui_input_list(usr, "Please choose a language to remove.","Language", H.languages) + var/datum/language/rem_language = tgui_input_list(src, "Please choose a language to remove.","Language", H.languages) if(!rem_language) return if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return if(H.remove_language(rem_language.name)) - to_chat(usr, "Removed [rem_language] from [H].") + to_chat(src, "Removed [rem_language] from [H].") else - to_chat(usr, "Mob doesn't know that language.") + to_chat(src, "Mob doesn't know that language.") else if(href_list["addverb"]) if(!check_rights(R_DEBUG)) return @@ -414,7 +414,7 @@ var/mob/H = locate(href_list["addverb"]) if(!ismob(H)) - to_chat(usr, "This can only be done to instances of type /mob") + to_chat(src, "This can only be done to instances of type /mob") return var/list/possibleverbs = list() possibleverbs += "Cancel" // One for the top... @@ -434,9 +434,9 @@ possibleverbs -= H.verbs possibleverbs += "Cancel" // ...And one for the bottom - var/verb = tgui_input_list(usr, "Select a verb!", "Verbs", possibleverbs) + var/verb = tgui_input_list(src, "Select a verb!", "Verbs", possibleverbs) if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return if(!verb || verb == "Cancel") return @@ -449,11 +449,11 @@ var/mob/H = locate(href_list["remverb"]) if(!istype(H)) - to_chat(usr, "This can only be done to instances of type /mob") + to_chat(src, "This can only be done to instances of type /mob") return - var/verb = tgui_input_list(usr, "Please choose a verb to remove.","Verbs", H.verbs) + var/verb = tgui_input_list(src, "Please choose a verb to remove.","Verbs", H.verbs) if(!H) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return if(!verb) return @@ -465,18 +465,18 @@ var/mob/living/carbon/M = locate(href_list["addorgan"]) if(!istype(M)) - to_chat(usr, "This can only be done to instances of type /mob/living/carbon") + to_chat(src, "This can only be done to instances of type /mob/living/carbon") return - var/new_organ = tgui_input_list(usr, "Please choose an organ to add.","Organ", subtypesof(/obj/item/organ)) + var/new_organ = tgui_input_list(src, "Please choose an organ to add.","Organ", subtypesof(/obj/item/organ)) if(!new_organ) return if(!M) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return if(locate(new_organ) in M.internal_organs) - to_chat(usr, "Mob already has that organ.") + to_chat(src, "Mob already has that organ.") return new new_organ(M) @@ -487,20 +487,20 @@ var/mob/living/carbon/M = locate(href_list["remorgan"]) if(!istype(M)) - to_chat(usr, "This can only be done to instances of type /mob/living/carbon") + to_chat(src, "This can only be done to instances of type /mob/living/carbon") return - var/obj/item/organ/rem_organ = tgui_input_list(usr, "Please choose an organ to remove.","Organ", M.internal_organs) + var/obj/item/organ/rem_organ = tgui_input_list(src, "Please choose an organ to remove.","Organ", M.internal_organs) if(!M) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return if(!(locate(rem_organ) in M.internal_organs)) - to_chat(usr, "Mob does not have that organ.") + to_chat(src, "Mob does not have that organ.") return - to_chat(usr, "Removed [rem_organ] from [M].") + to_chat(src, "Removed [rem_organ] from [M].") rem_organ.removed() qdel(rem_organ) @@ -510,12 +510,12 @@ var/mob/H = locate(href_list["fix_nano"]) if(!istype(H) || !H.client) - to_chat(usr, "This can only be done on mobs with clients") + to_chat(src, "This can only be done on mobs with clients") return H.client.send_resources() - to_chat(usr, "Resource files sent") + to_chat(src, "Resource files sent") to_chat(H, "Your NanoUI Resource files have been refreshed") log_admin("[key_name(usr)] resent the NanoUI resource files to [key_name(H)] ") @@ -525,7 +525,7 @@ var/mob/M = locate(href_list["regenerateicons"]) if(!ismob(M)) - to_chat(usr, "This can only be done to instances of type /mob") + to_chat(src, "This can only be done to instances of type /mob") return M.regenerate_icons() @@ -537,10 +537,10 @@ var/Text = href_list["adjustDamage"] - var/amount = tgui_input_number(usr, "Deal how much damage to mob? (Negative values here heal)","Adjust [Text]loss",0, min_value=-INFINITY, round_value=FALSE) + var/amount = tgui_input_number(src, "Deal how much damage to mob? (Negative values here heal)","Adjust [Text]loss",0, min_value=-INFINITY, round_value=FALSE) if(!L) - to_chat(usr, "Mob doesn't exist anymore") + to_chat(src, "Mob doesn't exist anymore") return switch(Text) @@ -551,7 +551,7 @@ if("brain") L.adjustBrainLoss(amount) if("clone") L.adjustCloneLoss(amount) else - to_chat(usr, "You caused an error. DEBUG: Text:[Text] Mob:[L]") + to_chat(src, "You caused an error. DEBUG: Text:[Text] Mob:[L]") return if(amount != 0) @@ -570,11 +570,11 @@ var/client/C = value["value"] if (!C) return - var/prompt = tgui_alert(usr, "Do you want to grant [C] access to view this VV window? (they will not be able to edit or change anysrc nor open nested vv windows unless they themselves are an admin)", "Confirm", list("Yes", "No")) + var/prompt = tgui_alert(src, "Do you want to grant [C] access to view this VV window? (they will not be able to edit or change anysrc nor open nested vv windows unless they themselves are an admin)", "Confirm", list("Yes", "No")) if (prompt != "Yes") return if(!thing) - to_chat(usr, span_warning("The object you tried to expose to [C] no longer exists (GC'd)")) + to_chat(src, span_warning("The object you tried to expose to [C] no longer exists (GC'd)")) return message_admins("[key_name_admin(usr)] Showed [key_name_admin(C)] a VV window") log_admin("Admin [key_name(usr)] Showed [key_name(C)] a VV window of a [src]") diff --git a/code/modules/admin/view_variables/view_variables.dm b/code/modules/admin/view_variables/view_variables.dm index 862f069326..98dc6b89e7 100644 --- a/code/modules/admin/view_variables/view_variables.dm +++ b/code/modules/admin/view_variables/view_variables.dm @@ -1,6 +1,10 @@ #define ICON_STATE_CHECKED 1 /// this dmi is checked. We don't check this one anymore. #define ICON_STATE_NULL 2 /// this dmi has null-named icon_state, allowing it to show a sprite on vv editor. +ADMIN_VERB_AND_CONTEXT_MENU(debug_variables, (R_DEBUG|R_SERVER|R_ADMIN|R_SPAWN|R_FUN|R_EVENT), "View Variables", "View the variables of a datum.", "Debug.Investigate", datum/thing in world) + user.debug_variables(thing) +// This is kept as a separate proc because admins are able to show VV to non-admins + /client/proc/debug_variables(datum/D in world) set category = "Debug.Investigate" set name = "View Variables" diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index eaa98f36e2..28d5018faa 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -179,7 +179,6 @@ switch(href_list["_src_"]) if("holder") hsrc = holder - if("mentorholder") hsrc = (check_rights(R_ADMIN, 0) ? holder : mentorholder) if("usr") hsrc = mob if("prefs") return prefs.process_link(usr,href_list) if("vars") return view_var_Topic(href,href_list,hsrc) @@ -257,8 +256,7 @@ initialize_commandbar_spy() tgui_panel = new(src, "browseroutput") - GLOB.ahelp_tickets.ClientLogin(src) - GLOB.mhelp_tickets.ClientLogin(src) + GLOB.tickets.ClientLogin(src) //Admin Authorisation holder = GLOB.admin_datums[ckey] @@ -266,10 +264,6 @@ GLOB.admins += src holder.owner = src - mentorholder = mentor_datums[ckey] - if (mentorholder) - mentorholder.associate(GLOB.directory[ckey]) - //preferences datum - also holds some persistant data for the client (because we may as well keep these datums to a minimum) prefs = preferences_datums[ckey] if(prefs) @@ -374,8 +368,7 @@ if (!QDELING(src)) stack_trace("Client does not purport to be QDELING, this is going to cause bugs in other places!") - GLOB.ahelp_tickets.ClientLogout(src) - GLOB.mhelp_tickets.ClientLogout(src) + GLOB.tickets.ClientLogout(src) // Yes this is the same as what's found in qdel(). Yes it does need to be here // Get off my back @@ -387,9 +380,6 @@ if(holder) holder.owner = null GLOB.admins -= src - if (mentorholder) - mentorholder.owner = null - GLOB.mentors -= src GLOB.directory -= ckey GLOB.clients -= src diff --git a/code/modules/client/verbs/who.dm b/code/modules/client/verbs/who.dm index 186be4369f..7825ff4b3e 100644 --- a/code/modules/client/verbs/who.dm +++ b/code/modules/client/verbs/who.dm @@ -62,10 +62,12 @@ var/modmsg = "" var/devmsg = "" var/eventMmsg = "" + var/mentormsg = "" var/num_mods_online = 0 var/num_admins_online = 0 var/num_devs_online = 0 var/num_event_managers_online = 0 + var/num_mentors_online = 0 for(var/client/C in GLOB.admins) // VOREStation Edit - GLOB var/temp = "" var/category = R_ADMIN @@ -84,6 +86,9 @@ else if(check_rights_for(C, R_STEALTH)) // event managers //VOREStation Edit: Retired Staff category = R_EVENT num_event_managers_online++ + else if(check_rights_for(C, R_MENTOR)) + category = R_MENTOR + num_mentors_online++ temp += "\t[C] is a [C.holder.rank_names()]" if(holder) @@ -110,6 +115,8 @@ devmsg += temp if(R_EVENT) eventMmsg += temp + if(R_MENTOR) + mentormsg += temp msg = span_bold("Current Admins ([num_admins_online]):") + "\n" + msg @@ -122,27 +129,8 @@ if(CONFIG_GET(flag/show_event_managers)) msg += "\n" + span_bold(" Current Miscellaneous ([num_event_managers_online]):") + "\n" + eventMmsg - var/num_mentors_online = 0 - var/mmsg = "" - - for(var/client/C in GLOB.mentors) - num_mentors_online++ - mmsg += "\t[C] is a Mentor" - if(holder) - if(isobserver(C.mob)) - mmsg += " - Observing" - else if(isnewplayer(C.mob)) - mmsg += " - Lobby" - else - mmsg += " - Playing" - - if(C.is_afk()) - var/seconds = C.last_activity_seconds() - mmsg += " (AFK - [round(seconds / 60)] minutes, [seconds % 60] seconds)" - mmsg += "\n" - if(CONFIG_GET(flag/show_mentors)) - msg += "\n" + span_bold(" Current Mentors ([num_mentors_online]):") + "\n" + mmsg + msg += "\n" + span_bold(" Current Mentors ([num_mentors_online]):") + "\n" + mentormsg msg += "\n" + span_info("Adminhelps are also sent to Discord. If no admins are available in game try anyway and an admin on Discord may see it and respond.") diff --git a/code/modules/mentor/mentor.dm b/code/modules/mentor/mentor.dm deleted file mode 100644 index 0c000386e5..0000000000 --- a/code/modules/mentor/mentor.dm +++ /dev/null @@ -1,268 +0,0 @@ -/client - var/datum/mentor/mentorholder = null - -var/list/mentor_datums = list() - -var/list/mentor_verbs_default = list( - /client/proc/cmd_mentor_ticket_panel, - /client/proc/cmd_mentor_say, - /client/proc/cmd_dementor -) - -/datum/mentor - var/client/owner = null - -/datum/mentor/New(ckey) - if(!ckey) - error("Mentor datum created without a ckey argument. Datum has been deleted") - qdel(src) - return - mentor_datums[ckey] = src - -/datum/mentor/proc/associate(client/C) - if(istype(C)) - owner = C - owner.mentorholder = src - owner.add_mentor_verbs() - GLOB.mentors |= C - -/datum/mentor/proc/disassociate() - if(owner) - GLOB.mentors -= owner - owner.remove_mentor_verbs() - owner.mentorholder = null - mentor_datums[owner.ckey] = null - qdel(src) - -/client/proc/add_mentor_verbs() - if(mentorholder) - add_verb(src, mentor_verbs_default) - -/client/proc/remove_mentor_verbs() - if(mentorholder) - remove_verb(src, mentor_verbs_default) - -/client/proc/make_mentor() - set category = "Admin.Secrets" - set name = "Make Mentor" - if(!holder) - to_chat(src, span_admin_pm_warning("Error: Only administrators may use this command.")) - return - var/list/client/targets[0] - for(var/client/T in GLOB.clients) - targets["[T.key]"] = T - var/target = tgui_input_list(src,"Who do you want to make a mentor?","Make Mentor", sortList(targets)) - if(!target) - return - var/client/C = targets[target] - if(has_mentor_powers(C) || GLOB.deadmins[C.ckey]) // If an admin is deadminned you could mentor them and that will cause fuckery if they readmin - to_chat(src, span_admin_pm_warning("Error: They already have mentor powers.")) - return - var/datum/mentor/M = new /datum/mentor(C.ckey) - M.associate(C) - to_chat(C, span_admin_pm_notice("You have been granted mentorship.")) - to_chat(src, span_admin_pm_notice("You have made [C] a mentor.")) - log_admin("[key_name(src)] made [key_name(C)] a mentor.") - feedback_add_details("admin_verb","Make Mentor") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/unmake_mentor() - set category = "Admin.Secrets" - set name = "Unmake Mentor" - if(!holder) - to_chat(src, span_admin_pm_warning("Error: Only administrators may use this command.")) - return - var/list/client/targets[0] - for(var/client/T in GLOB.mentors) - targets["[T.key]"] = T - var/target = tgui_input_list(src,"Which mentor do you want to unmake?","Unmake Mentor", sortList(targets)) - if(!target) - return - var/client/C = targets[target] - C.mentorholder.disassociate() - to_chat(C, span_admin_pm_warning("Your mentorship has been revoked.")) - to_chat(src, span_admin_pm_notice("You have revoked [C]'s mentorship.")) - log_admin("[key_name(src)] revoked [key_name(C)]'s mentorship.") - feedback_add_details("admin_verb","Unmake Mentor") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/cmd_mentor_say(msg as text) - set category = "Admin.Chat" - set name ="Mentorsay" - - //check rights - if (!has_mentor_powers(src)) - return - - msg = sanitize(msg) - if (!msg) - return - - log_admin("Mentorsay: [key_name(src)]: [msg]") - - for(var/client/C in GLOB.mentors) - to_chat(C, create_text_tag("mentor", "MENTOR:", C) + " " + span_mentor_channel(span_name("[src]") + ": " + span_message("[msg]"))) - for(var/client/C in GLOB.admins) - to_chat(C, create_text_tag("mentor", "MENTOR:", C) + " " + span_mentor_channel(span_name("[src]") + ": " + span_message("[msg]"))) - -/proc/mentor_commands(href, href_list, client/C) - if(href_list["mhelp"]) - var/mhelp_ref = href_list["mhelp"] - var/datum/mentor_help/MH = locate(mhelp_ref) - if (MH && istype(MH, /datum/mentor_help)) - MH.Action(href_list["mhelp_action"]) - else - to_chat(C, "Ticket [mhelp_ref] has been deleted!") - - if (href_list["mhelp_tickets"]) - GLOB.mhelp_tickets.BrowseTickets(text2num(href_list["mhelp_tickets"])) - - -/datum/mentor/Topic(href, href_list) - ..() - if (usr.client != src.owner || (!usr.client.mentorholder)) - log_admin("[key_name(usr)] tried to illegally use mentor functions.") - message_admins("[usr.key] tried to illegally use mentor functions.") - return - - mentor_commands(href, href_list, usr) - -/client/proc/cmd_dementor() - set category = "Admin.Misc" - set name = "De-mentor" - - if(tgui_alert(usr, "Confirm self-dementor for the round? You can't re-mentor yourself without someone promoting you.","Dementor",list("Yes","No")) == "Yes") - src.mentorholder.disassociate() - -/client/proc/cmd_mhelp_reply(whom) - if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, span_admin_pm_warning("Error: Mentor-PM: You are unable to use admin PM-s (muted).")) - return - var/client/C - if(istext(whom)) - C = GLOB.directory[whom] - else if(istype(whom,/client)) - C = whom - if(!C) - if(has_mentor_powers(src)) - to_chat(src, span_admin_pm_warning("Error: Mentor-PM: Client not found.")) - return - - var/datum/mentor_help/MH = C.current_mentorhelp - - if(MH) - message_mentors(span_mentor_channel("[src] has started replying to [C]'s mentor help.")) - var/msg = tgui_input_text(src,"Message:", "Private message to [C]", multiline = TRUE) - if (!msg) - message_mentors(span_mentor_channel("[src] has cancelled their reply to [C]'s mentor help.")) - return - cmd_mentor_pm(whom, msg, MH) - -/proc/has_mentor_powers(client/C) - return C.holder || C.mentorholder - -// This not really a great place to put it, but this verb replaces adminhelp in hotkeys so that people requesting help can select the type they need -// You can still directly adminhelp if necessary, this ONLY replaces the inbuilt hotkeys - -/client/verb/requesthelp() - set category = "Admin" - set name = "Request help" - set hidden = 1 - - var/mhelp = tgui_alert(usr, "Select the help you need.","Request for Help",list("Adminhelp","Mentorhelp")) - if(!mhelp) - return - - var/msg = tgui_input_text(usr, "Input your request for help.", "Request for Help ([mhelp])", multiline = TRUE) - if(!msg) - return - - if (mhelp == "Mentorhelp") - mentorhelp(msg) - return - - adminhelp(msg) - - -/client/proc/cmd_mentor_pm(whom, msg, datum/mentor_help/MH) - set category = "Admin" - set name = "Mentor-PM" - set hidden = 1 - - if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, span_mentor_pm_warning("Error: Mentor-PM: You are unable to use mentor PM-s (muted).")) - return - - //Not a mentor and no open ticket - if(!has_mentor_powers(src) && !current_mentorhelp) - to_chat(src, span_mentor_pm_warning("You can no longer reply to this ticket, please open another one by using the Mentorhelp verb if need be.")) - to_chat(src, span_mentor_pm_notice("Message: [msg]")) - return - - var/client/recipient - - if(istext(whom)) - recipient = GLOB.directory[whom] - - else if(istype(whom,/client)) - recipient = whom - - //get message text, limit it's length.and clean/escape html - if(!msg) - msg = tgui_input_text(src,"Message:", "Mentor-PM to [whom]", multiline = TRUE) - - if(!msg) - return - - if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, span_mentor_pm_warning("Error: Mentor-PM: You are unable to use mentor PM-s (muted).")) - return - - if(!recipient) - if(has_mentor_powers(src)) - to_chat(src, span_mentor_pm_warning("Error:Mentor-PM: Client not found.")) - to_chat(src, msg) - else - log_admin("Mentorhelp: [key_name(src)]: [msg]") - current_mentorhelp.MessageNoRecipient(msg) - return - - //Has mentor powers but the recipient no longer has an open ticket - if(has_mentor_powers(src) && !recipient.current_mentorhelp) - to_chat(src, span_mentor_pm_warning("You can no longer reply to this ticket.")) - to_chat(src, span_mentor_pm_notice("Message: [msg]")) - return - - if (src.handle_spam_prevention(msg,MUTE_ADMINHELP)) - return - - msg = trim(sanitize(copytext(msg,1,MAX_MESSAGE_LEN))) - if(!msg) - return - - var/interaction_message = span_mentor_pm_notice("Mentor-PM from-[src] to-[recipient]: [msg]") - - if (recipient.current_mentorhelp && !has_mentor_powers(recipient)) - recipient.current_mentorhelp.AddInteraction(interaction_message) - if (src.current_mentorhelp && !has_mentor_powers(src)) - src.current_mentorhelp.AddInteraction(interaction_message) - - // It's a little fucky if they're both mentors, but while admins may need to adminhelp I don't really see any reason a mentor would have to mentorhelp since you can literally just ask any other mentors online - if (has_mentor_powers(recipient) && has_mentor_powers(src)) - if (recipient.current_mentorhelp) - recipient.current_mentorhelp.AddInteraction(interaction_message) - if (src.current_mentorhelp) - src.current_mentorhelp.AddInteraction(interaction_message) - - to_chat(recipient, span_mentor(span_italics("Mentor-PM from-[src]: [msg]"))) - to_chat(src, span_mentor(span_italics("Mentor-PM to-[recipient]: [msg]"))) - - log_admin("[key_name(src)]->[key_name(recipient)]: [msg]") - - if(recipient.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) - recipient << 'sound/effects/mentorhelp.mp3' - - for(var/client/C in GLOB.mentors) - if (C != recipient && C != src) - to_chat(C, interaction_message) - for(var/client/C in GLOB.admins) - if (C != recipient && C != src) - to_chat(C, interaction_message) diff --git a/code/modules/mentor/mentorhelp.dm b/code/modules/mentor/mentorhelp.dm deleted file mode 100644 index 30967a8b80..0000000000 --- a/code/modules/mentor/mentorhelp.dm +++ /dev/null @@ -1,507 +0,0 @@ -/client/var/datum/mentor_help/current_mentorhelp - -// -//TICKET MANAGER -// - -GLOBAL_DATUM_INIT(mhelp_tickets, /datum/mentor_help_tickets, new) - -/datum/mentor_help_tickets - var/list/active_tickets = list() - var/list/resolved_tickets = list() - - var/obj/effect/statclick/mticket_list/astatclick = new(null, null, AHELP_ACTIVE) - var/obj/effect/statclick/mticket_list/rstatclick = new(null, null, AHELP_RESOLVED) - -/datum/mentor_help_tickets/Destroy() - QDEL_LIST(active_tickets) - QDEL_LIST(resolved_tickets) - QDEL_NULL(astatclick) - QDEL_NULL(rstatclick) - return ..() - -//private -/datum/mentor_help_tickets/proc/ListInsert(datum/mentor_help/new_ticket) - var/list/mticket_list - switch(new_ticket.state) - if(AHELP_ACTIVE) - mticket_list = active_tickets - if(AHELP_RESOLVED) - mticket_list = resolved_tickets - else - CRASH("Invalid ticket state: [new_ticket.state]") - var/num_closed = mticket_list.len - if(num_closed) - for(var/I in 1 to num_closed) - var/datum/mentor_help/MH = mticket_list[I] - if(MH.id > new_ticket.id) - mticket_list.Insert(I, new_ticket) - return - mticket_list += new_ticket - -//opens the ticket listings, only two states here -/datum/mentor_help_tickets/proc/BrowseTickets(state) - var/list/l2b - var/title - switch(state) - if(AHELP_ACTIVE) - l2b = active_tickets - title = "Active Tickets" - if(AHELP_RESOLVED) - l2b = resolved_tickets - title = "Resolved Tickets" - if(!l2b) - return - var/list/dat = list() - dat += "Refresh

" - for(var/datum/mentor_help/MH as anything in l2b) - dat += span_adminnotice(span_adminhelp("Ticket #[MH.id]") + " [MH.initiator_ckey]: [MH.name]") + "
" - - var/datum/browser/popup = new(usr, "mhelp_list[state]", "Mentor Help List", 600, 480) - popup.add_head_content("[title]") - popup.set_content(dat.Join()) - popup.open() - -//Tickets statpanel -/datum/mentor_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("== Mentor Tickets ==", "", null, null) - L[++L.len] = list("Active Tickets:", "[astatclick.update("[active_tickets.len]")]", null, REF(astatclick)) - astatclick.update("[active_tickets.len]") - for(var/datum/mentor_help/MH as anything in active_tickets) - if(MH.initiator) - L[++L.len] = list("#[MH.id]. [MH.initiator_ckey]:", "[MH.statclick.update()]", REF(MH)) - else - ++num_disconnected - if(num_disconnected) - L[++L.len] = list("Disconnected:", "[astatclick.update("[num_disconnected]")]", null, REF(astatclick)) - L[++L.len] = list("Resolved Tickets:", "[rstatclick.update("[resolved_tickets.len]")]", null, REF(rstatclick)) - return L - -//Reassociate still open ticket if one exists -/datum/mentor_help_tickets/proc/ClientLogin(client/C) - C.current_mentorhelp = CKey2ActiveTicket(C.ckey) - if(C.current_mentorhelp) - C.current_mentorhelp.AddInteraction("Client reconnected.") - C.current_mentorhelp.initiator = C - -//Dissasociate ticket -/datum/mentor_help_tickets/proc/ClientLogout(client/C) - if(C.current_mentorhelp) - C.current_mentorhelp.AddInteraction("Client disconnected.") - C.current_mentorhelp.initiator = null - C.current_mentorhelp = null - -//Get a ticket given a ckey -/datum/mentor_help_tickets/proc/CKey2ActiveTicket(ckey) - for(var/datum/admin_help/MH as anything in active_tickets) - if(MH.initiator_ckey == ckey) - return MH - -// -//TICKET LIST STATCLICK -// - -/obj/effect/statclick/mticket_list - var/current_state - -INITIALIZE_IMMEDIATE(/obj/effect/statclick/mticket_list) -/obj/effect/statclick/mticket_list/Initialize(mapload, name, state) - . = ..() - current_state = state - -/obj/effect/statclick/mticket_list/Click() - GLOB.mhelp_tickets.BrowseTickets(current_state) - -// -//TICKET DATUM -// - -/datum/mentor_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_mentorhelp -//msg is the title of the ticket: usually the ahelp text -/datum/mentor_help/New(msg, client/C) - //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 = C.ckey - initiator_key_name = key_name(initiator, FALSE, TRUE) - if(initiator.current_mentorhelp) //This is a bug - log_debug("Ticket erroneously left open by code") - initiator.current_mentorhelp.AddInteraction("Ticket erroneously left open by code") - initiator.current_mentorhelp.Resolve() - initiator.current_mentorhelp = src - - statclick = new(null, src) - _interactions = list() - - log_admin("Mentorhelp: [key_name(C)]: [msg]") - MessageNoRecipient(msg) - //show it to the person adminhelping too - to_chat(C, span_mentor(span_italics("Mentor-PM to-" + span_bold("Mentors") + ": [name]"))) - - GLOB.mhelp_tickets.active_tickets += src - -/datum/mentor_help/Destroy() - RemoveActive() - GLOB.mhelp_tickets.resolved_tickets -= src - return ..() - -/datum/mentor_help/proc/AddInteraction(formatted_message) - _interactions += "[gameTimestamp()]: [formatted_message]" - -//private -/datum/mentor_help/proc/ClosureLinks(ref_src) - if(!ref_src) - ref_src = "\ref[src]" - . = " (RSLVE)" - -//private -/datum/mentor_help/proc/LinkedReplyName(ref_src) - if(!ref_src) - ref_src = "\ref[src]" - return "[initiator_ckey]" - -//private -/datum/mentor_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 people with mentor powers will see this -/datum/mentor_help/proc/MessageNoRecipient(msg) - var/ref_src = "\ref[src]" - var/chat_msg = span_notice("(ESCALATE) Ticket [TicketHref("#[id]", ref_src)]" + span_bold(": [LinkedReplyName(ref_src)]:") + " [msg]") - AddInteraction(span_red("[LinkedReplyName(ref_src)]: [msg]")) - for (var/client/C in GLOB.mentors) - if (C.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) - C << 'sound/effects/mentorhelp.mp3' - for (var/client/C in GLOB.admins) - if (C.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) - C << 'sound/effects/mentorhelp.mp3' - message_mentors(chat_msg) - -//Reopen a closed ticket -/datum/mentor_help/proc/Reopen() - if(state == AHELP_ACTIVE) - to_chat(usr, span_warning("This ticket is already open.")) - return - - if(GLOB.mhelp_tickets.CKey2ActiveTicket(initiator_ckey)) - to_chat(usr, span_warning("This user already has an active ticket, cannot reopen this one.")) - return - - statclick = new(null, src) - GLOB.mhelp_tickets.active_tickets += src - GLOB.mhelp_tickets.resolved_tickets -= src - switch(state) - if(AHELP_RESOLVED) - feedback_dec("mhelp_resolve") - state = AHELP_ACTIVE - closed_at = null - if(initiator) - initiator.current_mentorhelp = src - - AddInteraction(span_purple("Reopened by [usr.ckey]")) - if(initiator) - to_chat(initiator, span_filter_adminlog("[span_purple("Ticket [TicketHref("#[id]")] was reopened by [usr.ckey].")]")) - var/msg = span_adminhelp("Ticket [TicketHref("#[id]")] reopened by [usr.ckey].") - message_mentors(msg) - log_admin(msg) - feedback_inc("mhelp_reopen") - TicketPanel() //can only be done from here, so refresh it - -//private -/datum/mentor_help/proc/RemoveActive() - if(state != AHELP_ACTIVE) - return - closed_at = world.time - QDEL_NULL(statclick) - GLOB.mhelp_tickets.active_tickets -= src - if(initiator && initiator.current_mentorhelp == src) - initiator.current_mentorhelp = null - -//Mark open ticket as resolved/legitimate, returns mentorhelp verb -/datum/mentor_help/proc/Resolve(silent = FALSE) - if(state != AHELP_ACTIVE) - return - RemoveActive() - state = AHELP_RESOLVED - GLOB.mhelp_tickets.ListInsert(src) - - AddInteraction(span_filter_adminlog("[span_green("Resolved by [usr.ckey].")]")) - if(initiator) - to_chat(initiator, span_filter_adminlog("[span_green("Ticket [TicketHref("#[id]")] was marked resolved by [usr.ckey].")]")) - if(!silent) - feedback_inc("mhelp_resolve") - var/msg = "Ticket [TicketHref("#[id]")] resolved by [usr.ckey]" - message_mentors(msg) - log_admin(msg) - -//Show the ticket panel -/datum/mentor_help/proc/TicketPanel() - tgui_interact(usr.client.mob) - -/datum/mentor_help/proc/TicketPanelLegacy() - var/list/dat = list() - var/ref_src = "\ref[src]" - dat += "

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

" - dat += span_bold("State: ") - switch(state) - if(AHELP_ACTIVE) - dat += span_bold(span_red("OPEN")) - if(AHELP_RESOLVED) - dat += span_bold(span_green("RESOLVED")) - else - dat += "UNKNOWN" - dat += "[GLOB.TAB][TicketHref("Refresh", ref_src)]" - 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 += span_bold("Actions:") + " [Context(ref_src)]
" - else - dat += span_bold("DISCONNECTED") + "[GLOB.TAB][ClosureLinks(ref_src)]
" - dat += "
" + span_bold("Log:") + "

" - for(var/I in _interactions) - dat += "[I]
" - - var/datum/browser/popup = new(usr, "mhelp[id]", "Mento Help [id]", 620, 480) - popup.add_head_content("Ticket #[id]") - popup.set_content(dat.Join()) - popup.open() - -/datum/mentor_help/tgui_fallback(payload) - if(..()) - return - - TicketPanelLegacy() - -/datum/mentor_help/tgui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "MentorTicketPanel", "Ticket #[id] - [LinkedReplyName("\ref[src]")]") - ui.open() - -/datum/mentor_help/tgui_state(mob/user) - return GLOB.tgui_mentor_state - -/datum/mentor_help/tgui_data(mob/user) - var/list/data = list() - - data["id"] = id - - var/ref_src = "\ref[src]" - data["title"] = name - data["name"] = LinkedReplyName(ref_src) - - switch(state) - if(AHELP_ACTIVE) - data["state"] = "open" - if(AHELP_RESOLVED) - data["state"] = "resolved" - else - data["state"] = "unknown" - - data["opened_at"] = (world.time - opened_at) - data["closed_at"] = (world.time - closed_at) - data["opened_at_date"] = gameTimestamp(wtime = opened_at) - data["closed_at_date"] = gameTimestamp(wtime = closed_at) - - data["actions"] = Context(ref_src) - - data["log"] = _interactions - - return data - -/datum/mentor_help/tgui_act(action, params) - if(..()) - return - switch(action) - if("escalate") - Escalate() - . = TRUE - if("reopen") - Reopen() - . = TRUE - if("legacy") - TicketPanelLegacy() - . = TRUE - -//Kick ticket to admins -/datum/mentor_help/proc/Escalate() - if(tgui_alert(usr, "Really escalate this ticket to admins? No mentors will ever be able to interact with it again if you do.","Escalate",list("Yes","No")) != "Yes") - return - if (src.initiator == null) // You can't escalate a mentorhelp of someone who's logged out because it won't create the adminhelp properly - to_chat(usr, span_mentor_pm_warning("Error: client not found, unable to escalate.")) - return - var/datum/admin_help/AH = new /datum/admin_help(src.name, src.initiator, FALSE) - message_mentors("[usr.ckey] escalated Ticket [TicketHref("#[id]")]") - log_admin("[key_name(usr)] escalated mentorhelp [src.name]") - to_chat(src.initiator, span_mentor("[usr.ckey] escalated your mentorhelp to admins.")) - AH._interactions = src._interactions - GLOB.mhelp_tickets.active_tickets -= src - GLOB.mhelp_tickets.resolved_tickets -= src - qdel(src) - -/datum/mentor_help/proc/Context(ref_src) - if(!ref_src) - ref_src = "\ref[src]" - if(state == AHELP_ACTIVE) - . += ClosureLinks(ref_src) - if(state != AHELP_RESOLVED) - . += " (ESCALATE)" - -//Forwarded action from admin/Topic OR mentor/Topic depending on which rank the caller has -/datum/mentor_help/proc/Action(action) - switch(action) - if("ticket") - TicketPanel() - if("reply") - usr.client.cmd_mhelp_reply(initiator) - if("resolve") - Resolve() - if("reopen") - Reopen() - if("escalate") - Escalate() - -// -// TICKET STATCLICK -// - -/obj/effect/statclick/mhelp - var/datum/mentor_help/mhelp_datum - -INITIALIZE_IMMEDIATE(/obj/effect/statclick/mhelp) -/obj/effect/statclick/mhelp/Initialize(mapload, datum/mentor_help/MH) - mhelp_datum = MH - . = ..() - -/obj/effect/statclick/mhelp/update() - return ..(mhelp_datum.name) - -/obj/effect/statclick/mhelp/Click() - mhelp_datum.TicketPanel() - -/obj/effect/statclick/mhelp/Destroy() - mhelp_datum = null - return ..() - -// -// CLIENT PROCS -// - -/client/verb/mentorhelp(msg as text) - set category = "Admin" - set name = "Mentorhelp" - - //handle muting and automuting - if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, span_danger("Error: Mentor-PM: You cannot send adminhelps (Muted).")) - return - if(handle_spam_prevention(msg,MUTE_ADMINHELP)) - return - - if(!msg) - return - - // Making sure there's actually a mentor or admin who can respond. - var/list/admins = get_admin_counts() - var/list/activeAdmins = admins["present"] - var/list/mentors = GLOB.mentors - if(!mentors.len && !activeAdmins.len) - var/choice = tgui_alert(usr, "There are no active admins or mentors online. Would you like to make an ahelp instead, so that staff is notified of your issue? \ - Alternatively, you may go to the discord yourself and repeat your question in #cadet-academy. Please note, if choosing the later, do not include current-round information.", - "Send to discord?", list("Admin-help!", "Still mentorhelp!", "Cancel")) - if(choice == "Admin-help!") - usr.client.adminhelp(msg) - remove_verb(src, /client/verb/mentorhelp) - spawn(1200) - add_verb(src, /client/verb/mentorhelp) // 2 minute cd to prevent abusing this to spam admins. - return - else if(!choice || choice == "Cancel") - return - - - - //remove out adminhelp verb temporarily to prevent spamming of admins. - remove_verb(src, /client/verb/mentorhelp) - spawn(600) - add_verb(src, /client/verb/mentorhelp) // 1 minute cool-down for mentorhelps - - feedback_add_details("admin_verb","Mentorhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - if(current_mentorhelp) - var/input = tgui_alert(usr, "You already have a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) - if(!input) - return - if(input == "Yes") - if(current_mentorhelp) - log_admin("Mentorhelp: [key_name(src)]: [msg]") - current_mentorhelp.MessageNoRecipient(msg) - to_chat(usr, span_mentor_pm_notice("Mentor-PM to-" + span_bold("Mentors") + ": [msg]")) - return - else - to_chat(usr, span_warning("Ticket not found, creating new one...")) - else - current_mentorhelp.AddInteraction("[usr.ckey] opened a new ticket.") - current_mentorhelp.Resolve() - - new /datum/mentor_help(msg, src, FALSE) - -//admin proc -/client/proc/cmd_mentor_ticket_panel() - set name = "Mentor Ticket List" - set category = "Admin.Misc" - - var/browse_to - - switch(tgui_input_list(usr, "Display which ticket list?", "List Choice", list("Active Tickets", "Resolved Tickets"))) - if("Active Tickets") - browse_to = AHELP_ACTIVE - if("Resolved Tickets") - browse_to = AHELP_RESOLVED - else - return - - GLOB.mhelp_tickets.BrowseTickets(browse_to) - -/proc/message_mentors(var/msg) - msg = span_mentor_channel(span_prefix("Mentor: " + span_message(msg))) - - for(var/client/C in GLOB.mentors) - to_chat(C, msg) - for(var/client/C in GLOB.admins) - to_chat(C, msg) diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index 64c5b5c051..722577899a 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -102,7 +102,7 @@ * * required payload list A list of the payload supposed to be set on the regular UI. */ -/datum/proc/tgui_fallback(list/payload) +/datum/proc/tgui_fallback(list/payload, mob/user) SHOULD_CALL_PARENT(TRUE) SEND_SIGNAL(src, COMSIG_UI_FALLBACK, usr) diff --git a/code/modules/tgui/states/mentor.dm b/code/modules/tgui/states/mentor.dm index fc2ab2b714..2c7e2aa751 100644 --- a/code/modules/tgui/states/mentor.dm +++ b/code/modules/tgui/states/mentor.dm @@ -7,6 +7,6 @@ GLOBAL_DATUM_INIT(tgui_mentor_state, /datum/tgui_state/mentor_state, new) /datum/tgui_state/mentor_state/can_use_topic(src_object, mob/user) - if(has_mentor_powers(user.client)) + if(check_rights_for(user.client, R_ADMIN|R_EVENT|R_DEBUG|R_MENTOR)) return STATUS_INTERACTIVE return STATUS_CLOSE diff --git a/code/modules/tgui/states/ticket.dm b/code/modules/tgui/states/ticket.dm new file mode 100644 index 0000000000..1f350e2e92 --- /dev/null +++ b/code/modules/tgui/states/ticket.dm @@ -0,0 +1,13 @@ +/** + * tgui state: ticket_state + * + * Grants the user UI_INTERACTIVE, if a ticket is open. + */ + +GLOBAL_DATUM_INIT(tgui_ticket_state, /datum/tgui_state/ticket_state, new) + +/datum/tgui_state/ticket_state/can_use_topic(src_object, mob/user) + //if (user.client.current_ticket) + // return STATUS_INTERACTIVE + //return STATUS_CLOSE + return STATUS_INTERACTIVE diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index 84b44a9daa..09d0a4b26c 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -430,7 +430,7 @@ #ifdef TGUI_DEBUGGING log_tgui(user, "Fallback Triggered: [href_list["payload"]], Window: [window.id], Source: [src_object]") #endif - src_object.tgui_fallback(payload) + src_object.tgui_fallback(payload, user) if(TGUI_MANAGED_BYONDUI_TYPE_RENDER) var/byond_ui_id = payload[TGUI_MANAGED_BYONDUI_PAYLOAD_ID] if(!byond_ui_id || LAZYLEN(open_byondui_elements) > TGUI_MANAGED_BYONDUI_LIMIT) diff --git a/code/modules/tgui_input/say_modal/speech.dm b/code/modules/tgui_input/say_modal/speech.dm index bed862e63a..a60ebe5767 100644 --- a/code/modules/tgui_input/say_modal/speech.dm +++ b/code/modules/tgui_input/say_modal/speech.dm @@ -55,7 +55,7 @@ return TRUE if(ADMIN_CHANNEL) if(check_rights(R_ADMIN, show_msg = FALSE)) - client.cmd_admin_say(entry) + SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/cmd_admin_say, entry) return TRUE return FALSE diff --git a/code/modules/tickets/procs.dm b/code/modules/tickets/procs.dm new file mode 100644 index 0000000000..2cd19a236d --- /dev/null +++ b/code/modules/tickets/procs.dm @@ -0,0 +1,295 @@ +// +// CLIENT PROCS +// + +/client/verb/mentorhelp(msg as text) + set category = "Admin" + set name = "Mentorhelp" + + //handle muting and automuting + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, span_danger("Error: Mentor-PM: You cannot send adminhelps (Muted).")) + return + if(handle_spam_prevention(msg,MUTE_ADMINHELP)) + return + + if(!msg) + return + + //remove out adminhelp verb temporarily to prevent spamming of admins. + remove_verb(src,/client/verb/mentorhelp) + spawn(600) + add_verb(src,/client/verb/mentorhelp ) // 1 minute cool-down for mentorhelps + + feedback_add_details("admin_verb","Mentorhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + if(current_ticket) + var/input = tgui_alert(src, "You already have a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) + if(!input) + return + if(input == "Yes") + if(current_ticket) + log_admin("Mentorhelp: [key_name(src)]: [msg]") + current_ticket.MessageNoRecipient(msg) + to_chat(src, span_adminnotice(span_mentor("Mentor-PM to-" + span_bold("Mentors") + ": [msg]"))) + return + else + to_chat(src, span_warning("Ticket not found, creating new one...")) + else + current_ticket.AddInteraction("[usr.ckey] opened a new ticket.") + current_ticket.Resolve() + + new /datum/ticket(msg, src, FALSE, 0) + +//admin proc +ADMIN_VERB(cmd_mentor_ticket_panel, (R_ADMIN|R_SERVER|R_MOD|R_MENTOR), "Mentor Ticket List", "Opens the list of mentor tickets", "Admin.Misc") + var/browse_to + + switch(tgui_input_list(user, "Display which ticket list?", "List Choice", list("Active Tickets", "Resolved Tickets"))) + if("Active Tickets") + browse_to = AHELP_ACTIVE + if("Resolved Tickets") + browse_to = AHELP_RESOLVED + else + return + + GLOB.tickets.BrowseTickets(browse_to) + +/proc/message_mentors(var/msg) + msg = span_mentor_channel(span_prefix("Mentor:") + span_message("[msg]")) + + for(var/client/C in GLOB.admins) + to_chat(C, msg) + +// +// CLIENT PROCS +// + +/client/verb/requesthelp() + set category = "Admin" + set name = "Request help" + set hidden = 1 + + var/mhelp = tgui_alert(usr, "Select the help you need.","Request for Help",list("Adminhelp","Mentorhelp")) + if(!mhelp) + return + + var/msg = tgui_input_text(usr, "Input your request for help.", "Request for Help ([mhelp])", multiline = TRUE) + if(!msg) + return + + if (mhelp == "Mentorhelp") + mentorhelp(msg) + return + + adminhelp(msg) + +/client/verb/adminhelp(msg as text) + set category = "Admin" + set name = "Adminhelp" + + //handle muting and automuting + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, span_danger("Error: Admin-PM: You cannot send adminhelps (Muted).")) + return + if(handle_spam_prevention(msg,MUTE_ADMINHELP)) + return + + if(!msg) + return + + //remove out adminhelp verb temporarily to prevent spamming of admins. + remove_verb(src,/client/verb/adminhelp) + spawn(1200) + add_verb(src,/client/verb/adminhelp ) // 2 minute cool-down for adminhelp + + 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) + var/input = tgui_alert(src, "You already have a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) + if(!input) + return + if(input == "Yes") + if(current_ticket) + current_ticket.MessageNoRecipient(msg) + to_chat(src, span_adminnotice("PM to-" + span_bold("Admins") + ": [msg]")) + return + else + to_chat(src, span_warning("Ticket not found, creating new one...")) + else if(current_ticket) + current_ticket.AddInteraction("[key_name_admin(usr)] opened a new ticket.") + current_ticket.Close() + + new /datum/ticket(msg, src, FALSE, 1) + +//admin proc +/client/proc/cmd_admin_ticket_panel() + set name = "Show Ticket List" + set category = "Admin.Misc" + + if(!check_rights(R_ADMIN|R_MOD|R_DEBUG|R_EVENT, TRUE)) + return + + var/browse_to + + switch(tgui_input_list(usr, "Display which ticket list?", "List Choice", 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.tickets.BrowseTickets(browse_to) + +//// VOREstation Additions Below + +/datum/ticket/proc/send2adminchat() + if(!CONFIG_GET(string/chat_webhook_url)) + return + + var/list/adm = get_admin_counts() + var/list/afkmins = adm["afk"] + var/list/allmins = adm["total"] + + spawn(0) //Unreliable world.Exports() + var/query_string = "type=adminhelp" + query_string += "&key=[url_encode(CONFIG_GET(string/chat_webhook_key))]" + query_string += "&from=[url_encode(key_name(initiator))]" + query_string += "&msg=[url_encode(html_decode(name))]" + query_string += "&admin_number=[allmins.len]" + query_string += "&admin_number_afk=[afkmins.len]" + world.Export("[CONFIG_GET(string/chat_webhook_url)]?[query_string]") + +/client/verb/adminspice() + set category = "Admin" + set name = "Request Spice" + set desc = "Request admins to spice round up for you" + + //handle muting and automuting + if(prefs.muted & MUTE_ADMINHELP) + to_chat(usr, span_danger("Error: You cannot request spice (muted from adminhelps).")) + return + + if(tgui_alert(usr, "Are you sure you want to request the admins spice things up for you? You accept the consequences if you do.","Spicy!",list("Yes","No")) == "Yes") + message_admins("[ADMIN_FULLMONTY(usr)] has requested the round be spiced up a little.") + to_chat(usr, span_notice("You have requested some more spice in your round.")) + else + to_chat(usr, span_notice("Spice request cancelled.")) + return + + //if they requested spice, then remove spice verb temporarily to prevent spamming + remove_verb(usr,/client/verb/adminspice) + spawn(10 MINUTES) + if(usr) // In case we left in the 10 minute cooldown + add_verb(usr,/client/verb/adminspice ) // 10 minute cool-down for spice request + +// +// MENTOR PROCS +// + +/client/proc/cmd_mhelp_reply(whom) + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, span_admin_pm_warning("Error: Mentor-PM: You are unable to use admin PM-s (muted).")) + return + var/client/C + if(istext(whom)) + C = GLOB.directory[whom] + else if(istype(whom,/client)) + C = whom + if(!C) + if(src.holder) + to_chat(src, span_admin_pm_warning("Error: Mentor-PM: Client not found.")) + return + + var/datum/ticket/T = C.current_ticket + + if(T) + message_mentors(span_mentor_channel("[src] has started replying to [C]'s mentor help.")) + var/msg = tgui_input_text(src,"Message:", "Private message to [C]", multiline = TRUE) + if (!msg) + message_mentors(span_mentor_channel("[src] has cancelled their reply to [C]'s mentor help.")) + return + cmd_mentor_pm(whom, msg, T) + +/client/proc/cmd_mentor_pm(whom, msg, datum/ticket/T) + set category = "Admin" + set name = "Mentor-PM" + set hidden = 1 + + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, span_mentor_warning("Error: Mentor-PM: You are unable to use mentor PM-s (muted).")) + return + + //Not a mentor and no open ticket + if(!src.holder && !current_ticket) + to_chat(src, span_mentor_warning("You can no longer reply to this ticket, please open another one by using the Mentorhelp verb if need be.")) + to_chat(src, span_mentor_notice("Message: [msg]")) + return + + var/client/recipient + + if(istext(whom)) + recipient = GLOB.directory[whom] + + else if(istype(whom,/client)) + recipient = whom + + //get message text, limit it's length.and clean/escape html + if(!msg) + msg = tgui_input_text(src,"Message:", "Mentor-PM to [whom]", multiline = TRUE) + + if(!msg) + return + + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, span_mentor_warning("Error: Mentor-PM: You are unable to use mentor PM-s (muted).")) + return + + if(!recipient) + if(src.holder) + to_chat(src, span_mentor_warning("Error:Mentor-PM: Client not found.")) + to_chat(src, msg) + else + log_admin("Mentorhelp: [key_name(src)]: [msg]") + current_ticket.MessageNoRecipient(msg) + return + + //Has mentor powers but the recipient no longer has an open ticket + if(src.holder && !recipient.current_ticket) + to_chat(src, span_mentor_warning("You can no longer reply to this ticket.")) + to_chat(src, span_mentor_notice("Message: [msg]")) + return + + if (src.handle_spam_prevention(msg,MUTE_ADMINHELP)) + return + + msg = trim(sanitize(copytext(msg,1,MAX_MESSAGE_LEN))) + if(!msg) + return + + var/interaction_message = span_mentor_notice("Mentor-PM from-[src] to-[recipient]: [msg]") + + if (recipient.current_ticket && !recipient.holder && recipient.current_ticket.level == 0) + recipient.current_ticket.AddInteraction(interaction_message) + if (src.current_ticket && !src.holder && src.current_ticket.level == 0) + src.current_ticket.AddInteraction(interaction_message) + + // It's a little fucky if they're both mentors, but while admins may need to adminhelp I don't really see any reason a mentor would have to mentorhelp since you can literally just ask any other mentors online + if (recipient.holder && src.holder) + if (recipient.current_ticket && recipient != src && recipient.current_ticket.level == 0) + recipient.current_ticket.AddInteraction(interaction_message) + if (src.current_ticket && src.current_ticket.level == 0) + src.current_ticket.AddInteraction(interaction_message) + + to_chat(recipient, span_mentor(span_italics("Mentor-PM from-[src]: [msg]"))) + to_chat(src, span_mentor(span_italics("Mentor-PM to-[recipient]: [msg]"))) + + log_admin("[key_name(src)]->[key_name(recipient)]: [msg]") + + if(recipient.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) + recipient << 'sound/effects/mentorhelp.mp3' + + for(var/client/C in GLOB.admins) + if (C != recipient && C != src) + to_chat(C, interaction_message) diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/tickets/tickets.dm similarity index 51% rename from code/modules/admin/verbs/adminhelp.dm rename to code/modules/tickets/tickets.dm index b0d73fdf22..34189fc23e 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/tickets/tickets.dm @@ -1,12 +1,32 @@ -/client/var/datum/admin_help/current_ticket //the current ticket the (usually) not-admin client is dealing with +/client/var/datum/ticket/current_ticket //the current ticket the (usually) not-admin client is dealing with +/client/var/datum/ticket/selected_ticket //the current ticket being viewed in the Tickets Panel (usually) admin/mentor client + +/proc/get_ahelp_channel() + var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs) + if(istype(api) && CONFIG_GET(string/ahelp_channel_tag)) + for(var/datum/tgs_chat_channel/channel in api.chat_channels) + if(channel.custom_tag == CONFIG_GET(string/ahelp_channel_tag)) + return list(channel) + return 0 + +/proc/ahelp_discord_message(var/message) + if(!message) + return + if(CONFIG_GET(flag/discord_ahelps_disabled)) + return + var/datum/tgs_chat_channel/ahelp_channel = get_ahelp_channel() + if(ahelp_channel) + world.TgsChatBroadcast(message,ahelp_channel) + else + world.TgsTargetedChatBroadcast(message,TRUE) // //TICKET MANAGER // -GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) +GLOBAL_DATUM_INIT(tickets, /datum/tickets, new) -/datum/admin_help_tickets +/datum/tickets var/list/active_tickets = list() var/list/closed_tickets = list() var/list/resolved_tickets = list() @@ -15,7 +35,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) 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() +/datum/tickets/Destroy() QDEL_LIST(active_tickets) QDEL_LIST(closed_tickets) QDEL_LIST(resolved_tickets) @@ -25,7 +45,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) return ..() //private -/datum/admin_help_tickets/proc/ListInsert(datum/admin_help/new_ticket) +/datum/tickets/proc/ListInsert(datum/ticket/new_ticket) var/list/ticket_list switch(new_ticket.state) if(AHELP_ACTIVE) @@ -39,16 +59,17 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) 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) + var/datum/ticket/T = ticket_list[I] + if(T.id > new_ticket.id) ticket_list.Insert(I, new_ticket) return ticket_list += new_ticket +/datum/tickets/proc/BrowseTickets(state) + tgui_interact(usr) + //opens the ticket listings for one of the 3 states -/datum/admin_help_tickets/proc/BrowseTickets(state) - if(!check_rights(R_ADMIN|R_SERVER)) //Prevents non-staff from opening the list of ahelp tickets - return +/datum/tickets/proc/BrowseTicketsLegacy(state) var/list/l2b var/title switch(state) @@ -65,50 +86,116 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) return var/list/dat = list("[title]") dat += "Refresh

" - for(var/datum/admin_help/AH as anything in l2b) - dat += span_adminnotice(span_adminhelp("Ticket #[AH.id]") + ": [AH.initiator_key_name]: [AH.name]") + "
" + for(var/datum/ticket/T as anything in l2b) + dat += span_adminnotice(span_adminhelp("Ticket #[T.id]") + ": [T.initiator_key_name]: [T.name]") + "
" dat += "" usr << browse(dat.Join(), "window=ahelp_list[state];size=600x480") //Tickets statpanel -/datum/admin_help_tickets/proc/stat_entry() +/datum/tickets/proc/stat_entry(client/target) SHOULD_CALL_PARENT(TRUE) SHOULD_NOT_SLEEP(TRUE) var/list/L = list() - var/num_disconnected = 0 - L[++L.len] = list("== Admin Tickets ==", "", null, null) - L[++L.len] = list("Active Tickets:", "[astatclick.update("[active_tickets.len]")]", null, REF(astatclick)) - astatclick.update("[active_tickets.len]") - for(var/datum/admin_help/AH as anything in active_tickets) - if(AH.initiator) - L[++L.len] = list("#[AH.id]. [AH.initiator_key_name]:", "[AH.statclick.update()]", REF(AH)) + var/num_adm_tickets_disconnected = 0 + var/num_men_tickets_disconnected = 0 + + var/list/admin_tickets = list() + var/list/mentor_tickets = list() + + for(var/datum/ticket/T as anything in active_tickets) + switch (T.level) + if(1) + admin_tickets += T + if(0) + mentor_tickets += T + + var/closed_admin_tickets = 0 + var/closed_mentor_tickets = 0 + + for(var/datum/ticket/T as anything in closed_tickets) + switch (T.level) + if(1) + closed_admin_tickets++ + if(0) + closed_mentor_tickets++ + + var/resolved_admin_tickets = 0 + var/resolved_mentor_tickets = 0 + + for(var/datum/ticket/T as anything in resolved_tickets) + switch (T.level) + if(1) + resolved_admin_tickets++ + if(0) + resolved_mentor_tickets++ + + if(check_rights_for(target, R_ADMIN|R_SERVER|R_MOD)) + L[++L.len] = list("== Admin Tickets ==", "", null, null) + L[++L.len] = list("Active Tickets:", "[astatclick.update("[admin_tickets.len]")]", null, REF(astatclick)) + for(var/datum/ticket/T as anything in admin_tickets) + if(T.initiator) + L[++L.len] = list("ADM #[T.id]. [T.initiator_key_name]:", T.name, null, REF(T.statclick)) + else + num_adm_tickets_disconnected++ + if(num_adm_tickets_disconnected) + L[++L.len] = list("Disconnected:", "[astatclick.update("[num_adm_tickets_disconnected]")]", null, REF(astatclick)) + L[++L.len] = list("Closed Tickets:", "[cstatclick.update("[closed_admin_tickets]")]", null, REF(cstatclick)) + L[++L.len] = list("Resolved Tickets:", "[rstatclick.update("[resolved_admin_tickets]")]", null, REF(rstatclick)) + + L[++L.len] = list("== Mentor Tickets ==", "", null, null) + L[++L.len] = list("Active Tickets:", "[astatclick.update("[mentor_tickets.len]")]", null, REF(astatclick)) + for(var/datum/ticket/T as anything in mentor_tickets) + if(T.initiator) + L[++L.len] = list("MEN #[T.id]. [T.initiator_key_name]:", T.name, null, REF(T.statclick)) 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)) + num_men_tickets_disconnected++ + if(num_men_tickets_disconnected) + L[++L.len] = list("Disconnected:", "[astatclick.update("[num_men_tickets_disconnected]")]", null, REF(astatclick)) + L[++L.len] = list("Closed Tickets:", "[cstatclick.update("[closed_mentor_tickets]")]", null, REF(cstatclick)) + L[++L.len] = list("Resolved Tickets:", "[rstatclick.update("[resolved_mentor_tickets]")]", null, REF(rstatclick)) + return L //Reassociate still open ticket if one exists -/datum/admin_help_tickets/proc/ClientLogin(client/C) +/datum/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 + // C.current_ticket.initiator.mob.throw_alert("open ticket", /obj/screen/alert/open_ticket) // Uncomment this line to enable player-side ticket ui //Dissasociate ticket -/datum/admin_help_tickets/proc/ClientLogout(client/C) +/datum/tickets/proc/ClientLogout(client/C) if(C.current_ticket) - C.current_ticket.AddInteraction("Client disconnected.") - C.current_ticket.initiator = null - C.current_ticket = null + var/datum/ticket/T = C.current_ticket + T.AddInteraction("Client disconnected.") + // T.initiator.mob.clear_alert("open ticket") // Uncomment this line to enable player-side ticket ui + T.initiator = null + T = null //Get a ticket given a ckey -/datum/admin_help_tickets/proc/CKey2ActiveTicket(ckey) - for(var/datum/admin_help/AH as anything in active_tickets) - if(AH.initiator_ckey == ckey) - return AH +/datum/tickets/proc/CKey2ActiveTicket(ckey) + for(var/datum/ticket/T as anything in active_tickets) + if(T.initiator_ckey == ckey) + return T + +//Get a ticket by ticket id +/datum/tickets/proc/ID2Ticket(id) + if(!check_rights((R_ADMIN|R_SERVER|R_MOD|R_MENTOR), TRUE)) + message_admins("[usr] has attempted to look up a ticket with ID [id] without sufficent privileges.") + return + + for(var/datum/ticket/T as anything in active_tickets) + if(T.id == id) + return T + + for(var/datum/ticket/T as anything in resolved_tickets) + if(T.id == id) + return T + + for(var/datum/ticket/T as anything in closed_tickets) + if(T.id == id) + return T // //TICKET LIST STATCLICK @@ -117,42 +204,70 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) /obj/effect/statclick/ticket_list var/current_state -/obj/effect/statclick/ticket_list/Initialize(mapload, text, target) +INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) +/obj/effect/statclick/ticket_list/Initialize(mapload, name, state) + current_state = state . = ..() - current_state = target /obj/effect/statclick/ticket_list/Click() - GLOB.ahelp_tickets.BrowseTickets(current_state) + GLOB.tickets.BrowseTickets(current_state) -//called by admin topic -/obj/effect/statclick/ticket_list/proc/Action() - Click() // //TICKET DATUM // -/datum/admin_help +/datum/ticket var/id var/name + var/level = 0 // 0 = Mentor, 1 = Admin + var/list/tags var/state = AHELP_ACTIVE var/opened_at var/closed_at var/client/initiator //semi-misnomer, it's the person who ahelped/was bwoinked + var/handler = "/Unassigned\\" // The admin handling the ticket var/initiator_ckey var/initiator_key_name var/list/_interactions //use AddInteraction() or, preferably, admin_ticket_log() - var/obj/effect/statclick/ahelp/statclick + var/obj/effect/statclick/ticket/statclick var/static/ticket_counter = 0 -//call this on its own to create a ticket, don't manually assign current_ticket +/* +//call this on its own to create a ticket, don't manually assign current_mentorhelp //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) +/datum/mentor_help/New(msg, client/C) + initiator_ckey = C.ckey + initiator_key_name = key_name(initiator, FALSE, TRUE) + if(initiator.current_mentorhelp) //This is a bug + log_debug("Ticket erroneously left open by code") + initiator.current_mentorhelp.AddInteraction("Ticket erroneously left open by code") + initiator.current_mentorhelp.Resolve() + initiator.current_mentorhelp = src + statclick = new(null, src) + _interactions = list() + log_admin("Mentorhelp: [key_name(C)]: [msg]") + MessageNoRecipient(msg) + //show it to the person adminhelping too + to_chat(C, span_mentor(span_italics("Mentor-PM to-" + span_bold("Mentors") + ": [name]")) + GLOB.ahelp_tickets.active_tickets += src */ + +/** + * public + * + * Create a new Ticket. + * Call this on its own to create a ticket, don't manually assign current_ticket + * + * required msg string The title of the ticket: usually the ahelp text + * required C client The object or datum which owns the UI. + * required is_bwoink boolean TRUE if this ticket was started by an admin PM + * required level integer The level of the ticket. 0 = Admin, 1 = Mentor + */ +/datum/ticket/New(msg, client/C, is_bwoink, ticket_level) //clean the input msg msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN)) if(!msg || !C || !C.mob) @@ -164,6 +279,8 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) name = msg + level = ticket_level + initiator = C initiator_ckey = initiator.ckey initiator_key_name = key_name(initiator, FALSE, TRUE) @@ -183,15 +300,28 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) message_admins(span_blue("Ticket [TicketHref("#[id]")] created")) else MessageNoRecipient(parsed_message) - send2adminchat() //VOREStation Add //show it to the person adminhelping too - to_chat(C, span_admin_pm_notice("PM to-" + span_bold("Admins") + ": [name]")) + switch(level) + if(0) + to_chat(C, span_mentor_notice("PM to-" + span_bold("Mentors") + ": [name]")) + + if(1) + to_chat(C, span_adminnotice("PM to-" + span_bold("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, span_admin_pm_notice("No active admins are online, your adminhelp was sent to the admin discord.")) //VOREStation Edit + to_chat(C, span_notice("No active admins are online, your adminhelp was sent to the admin discord.")) + send2adminchat() + + var/list/adm = get_admin_counts() + var/list/activemins = adm["present"] + var activeMins = activemins.len + if(is_bwoink) + ahelp_discord_message("ADMINHELP: FROM: [key_name_admin(usr)] TO [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") + else + ahelp_discord_message("ADMINHELP: FROM: [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") // Also send it to discord since that's the hip cool thing now. SSwebhooks.send( @@ -203,86 +333,127 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) ) ) - GLOB.ahelp_tickets.active_tickets += src + GLOB.tickets.active_tickets += src -/datum/admin_help/Destroy() + // Open a new chat with the user + //var/datum/ticket_chat/TC = new() + //TC.T = src + //TC.tgui_interact(C.mob) + + // C.mob.throw_alert("open ticket", /obj/screen/alert/open_ticket) // Uncomment this line to enable player-side ticket ui + +/datum/ticket/Destroy() RemoveActive() - GLOB.ahelp_tickets.closed_tickets -= src - GLOB.ahelp_tickets.resolved_tickets -= src + GLOB.tickets.closed_tickets -= src + GLOB.tickets.resolved_tickets -= src return ..() -/datum/admin_help/proc/AddInteraction(formatted_message) - _interactions += "[gameTimestamp()]: [formatted_message]" +/datum/ticket/proc/AddInteraction(formatted_message) + var/curinteraction = "[gameTimestamp()]: [formatted_message]" + if(CONFIG_GET(flag/discord_ahelps_all)) + ahelp_discord_message("ADMINHELP: TICKETID:[id] [strip_html_properly(curinteraction)]") + _interactions += curinteraction + +/datum/ticket/proc/TicketPanel() + tgui_interact(usr.client.mob) //private -/datum/admin_help/proc/FullMonty(ref_src) +/datum/ticket/proc/FullMonty(ref_src, admin = FALSE) if(!ref_src) ref_src = "\ref[src]" if(initiator && initiator.mob) - . = ADMIN_FULLMONTY_NONAME(initiator.mob) + if(admin) + . = ADMIN_FULLMONTY_NONAME(initiator.mob) else . = "Initiator disconnected." if(state == AHELP_ACTIVE) . += ClosureLinks(ref_src) //private -/datum/admin_help/proc/ClosureLinks(ref_src) +/datum/ticket/proc/ClosureLinks(ref_src) if(!ref_src) ref_src = "\ref[src]" - . = " (REJT)" - . += " (IC)" - . += " (CLOSE)" - . += " (RSLVE)" - . += " (HANDLE)" + + if(level == 1) + . = " (REJT)" + . += " (IC)" + . += " (CLOSE)" + . += " (RSLVE)" + . += " (HANDLE)" + else + . = " (RSLVE)" + . += " (ESCALATE)" //private -/datum/admin_help/proc/LinkedReplyName(ref_src) +/datum/ticket/proc/LinkedReplyName(ref_src) if(!ref_src) ref_src = "\ref[src]" - return "[initiator_key_name]" + return "[initiator_key_name]" //private -/datum/admin_help/proc/TicketHref(msg, ref_src, action = "ticket") +/datum/ticket/proc/TicketHref(msg, ref_src, action = "ticket") if(!ref_src) ref_src = "\ref[src]" - return "[msg]" + return "[msg]" + +/* + var/chat_msg = span_notice("(ESCALATE) Ticket [TicketHref("#[id]", ref_src)]" + span_bold(": [LinkedReplyName(ref_src)]:") + " [msg]") + */ //message from the initiator without a target, all admins will see this //won't bug irc -/datum/admin_help/proc/MessageNoRecipient(msg) +/datum/ticket/proc/MessageNoRecipient(msg) var/ref_src = "\ref[src]" var/chat_msg = span_admin_pm_notice(span_adminhelp("Ticket [TicketHref("#[id]", ref_src)]") + span_bold(": [LinkedReplyName(ref_src)] [FullMonty(ref_src)]:") + msg) AddInteraction(span_red("[LinkedReplyName(ref_src)]: [msg]")) //send this msg to all admins - for(var/client/X in GLOB.admins) - if(!check_rights_for(X, R_ADMIN)) - continue - if(X.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) - X << 'sound/effects/adminhelp.ogg' - window_flash(X) - to_chat(X, chat_msg) + if(level == 0) + for (var/client/C in GLOB.admins) + if (C.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) + C << 'sound/effects/mentorhelp.mp3' + message_mentors(chat_msg) + else if(level == 1) + for(var/client/X in GLOB.admins) + if(X.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) + X << 'sound/effects/adminhelp.ogg' + window_flash(X) + to_chat(X, chat_msg) + +/* +//Reopen a closed ticket +/datum/mentor_help/proc/Reopen() + switch(state) + if(AHELP_RESOLVED) + feedback_dec("ahelp_resolve") + AddInteraction(span_purple("Reopened by [usr.ckey]")) + if(initiator) + to_chat(initiator, span_filter_adminlog(span_purple("Ticket [TicketHref("#[id]")] was reopened by [usr.ckey]."))) + var/msg = span_adminhelp("Ticket [TicketHref("#[id]")] reopened by [usr.ckey].") + message_mentors(msg) + log_admin(msg) + */ //Reopen a closed ticket -/datum/admin_help/proc/Reopen() +/datum/ticket/proc/Reopen() if(state == AHELP_ACTIVE) to_chat(usr, span_warning("This ticket is already open.")) return - if(GLOB.ahelp_tickets.CKey2ActiveTicket(initiator_ckey)) + if(GLOB.tickets.CKey2ActiveTicket(initiator_ckey)) to_chat(usr, span_warning("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 + GLOB.tickets.active_tickets += src + GLOB.tickets.closed_tickets -= src + GLOB.tickets.resolved_tickets -= src switch(state) if(AHELP_CLOSED) - feedback_dec("ahelp_close") + feedback_dec("ticket_close") if(AHELP_RESOLVED) - feedback_dec("ahelp_resolve") + feedback_dec("ticket_resolve") state = AHELP_ACTIVE closed_at = null if(initiator) @@ -294,8 +465,8 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) var/msg = span_adminhelp("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 + feedback_inc("ticket_reopen") + //TicketPanel() //can only be done from here, so refresh it SSwebhooks.send( WEBHOOK_AHELP_SENT, @@ -306,22 +477,22 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) ) //private -/datum/admin_help/proc/RemoveActive() +/datum/ticket/proc/RemoveActive() if(state != AHELP_ACTIVE) return closed_at = world.time QDEL_NULL(statclick) - GLOB.ahelp_tickets.active_tickets -= src + GLOB.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(silent = FALSE) +/datum/ticket/proc/Close(silent = FALSE) if(state != AHELP_ACTIVE) return RemoveActive() state = AHELP_CLOSED - GLOB.ahelp_tickets.ListInsert(src) + GLOB.tickets.ListInsert(src) AddInteraction(span_filter_adminlog(span_red("Closed by [key_name_admin(usr)]."))) if(initiator) to_chat(initiator, span_filter_adminlog("[span_red("Ticket [TicketHref("#[id]")] was closed by [key_name(usr,FALSE,FALSE)].")]")) @@ -338,34 +509,41 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) "color" = COLOR_WEBHOOK_BAD ) ) + // initiator?.mob?.clear_alert("open ticket") // Uncomment this line to enable player-side ticket ui //Mark open ticket as resolved/legitimate, returns ahelp verb -/datum/admin_help/proc/Resolve(silent = FALSE) +/datum/ticket/proc/Resolve(silent = FALSE) if(state != AHELP_ACTIVE) return RemoveActive() state = AHELP_RESOLVED - GLOB.ahelp_tickets.ListInsert(src) + GLOB.tickets.ListInsert(src) AddInteraction(span_filter_adminlog(span_green("Resolved by [key_name_admin(usr)]."))) if(initiator) to_chat(initiator, span_filter_adminlog("[span_green("Ticket [TicketHref("#[id]")] was marked resolved by [key_name(usr,FALSE,FALSE)].")]")) if(!silent) - feedback_inc("ahelp_resolve") + feedback_inc("ticket_resolve") var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name_admin(usr)]" - message_admins(msg) + if(type == 1) + message_mentors(msg) + else if (type == 0) + message_admins(msg) + log_admin(msg) - SSwebhooks.send( - WEBHOOK_AHELP_SENT, - list( - "name" = "Ticket ([id]) (Game ID: [game_id]) resolved.", - "body" = "Marked as Resolved by [key_name(usr)].", - "color" = COLOR_WEBHOOK_GOOD + if(type == 1) + SSwebhooks.send( + WEBHOOK_AHELP_SENT, + list( + "name" = "Ticket ([id]) (Game ID: [game_id]) resolved.", + "body" = "Marked as Resolved by [key_name(usr)].", + "color" = COLOR_WEBHOOK_GOOD + ) ) - ) + // initiator?.mob?.clear_alert("open ticket") // Uncomment this line to enable player-side ticket ui //Close and return ahelp verb, use if ticket is incoherent -/datum/admin_help/proc/Reject(key_name = key_name_admin(usr)) +/datum/ticket/proc/Reject(key_name = key_name_admin(usr)) if(state != AHELP_ACTIVE) return @@ -393,7 +571,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) ) //Resolve ticket with IC Issue message -/datum/admin_help/proc/ICIssue(key_name = key_name_admin(usr)) +/datum/ticket/proc/ICIssue(key_name = key_name_admin(usr)) if(state != AHELP_ACTIVE) return @@ -402,7 +580,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) msg += "[span_red("Your AdminHelp may also be unanswerable due to ongoing events.")]" if(initiator) - to_chat(initiator,span_filter_pm(msg)) + to_chat(initiator, span_filter_pm(msg)) feedback_inc("ahelp_icissue") msg = "Ticket [TicketHref("#[id]")] marked as IC by [key_name_admin(usr)]" @@ -420,11 +598,20 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) ) //Resolve ticket with IC Issue message -/datum/admin_help/proc/HandleIssue() +/datum/ticket/proc/HandleIssue() if(state != AHELP_ACTIVE) return - var/msg = span_red("Your AdminHelp is being handled by [key_name(usr,FALSE,FALSE)] please be patient.") + if(handler == key_name(usr, FALSE, TRUE)) + to_chat(usr, span_red("You are already handling this ticket.")) + return + + var/msg + switch(level) + if(0) + msg = span_green("Your MentorHelp is being handled by [key_name(usr,FALSE,FALSE)] please be patient.") + if(1) + msg = span_red("Your AdminHelp is being handled by [key_name(usr,FALSE,FALSE)] please be patient.") if(initiator) to_chat(initiator, msg) @@ -434,51 +621,16 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) message_admins(msg) log_admin(msg) AddInteraction("[key_name_admin(usr)] is now handling this ticket.") - var/query_string = "type=admintake" - query_string += "&key=[url_encode(CONFIG_GET(string/chat_webhook_key))]" - query_string += "&admin=[url_encode(key_name(usr))]" - query_string += "&user=[url_encode(key_name(initiator))]" - world.Export("[CONFIG_GET(string/chat_webhook_url)]?[query_string]") + handler = key_name(usr, FALSE, TRUE) + SSwebhooks.send( + WEBHOOK_AHELP_SENT, + list( + "name" = "Ticket ([id]) (Game ID: [game_id]) being handled.", + "body" = "[key_name(usr)] is now handling the ticket." + ) + ) - - - -//Show the ticket panel -/datum/admin_help/proc/TicketPanel() - tgui_interact(usr.client.mob) - -/datum/admin_help/proc/TicketPanelLegacy() - var/list/dat = list("Ticket #[id]") - var/ref_src = "\ref[src]" - dat += "

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

" - dat += span_bold("State: ") - switch(state) - if(AHELP_ACTIVE) - dat += span_red(span_bold("OPEN")) - if(AHELP_RESOLVED) - dat += span_green(span_bold("RESOLVED")) - if(AHELP_CLOSED) - dat += span_bold("CLOSED") - else - dat += span_bold("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 += span_bold("Actions:") + " [FullMonty(ref_src)]
" - else - dat += span_bold("DISCONNECTED") + "[GLOB.TAB][ClosureLinks(ref_src)]
" - dat += "
" + span_bold("Log:") + "

" - for(var/I in _interactions) - dat += "[I]
" - dat += "" - usr << browse(dat.Join(), "window=ahelp[id];size=620x480") - -/datum/admin_help/proc/Retitle() +/datum/ticket/proc/Retitle() var/new_title = tgui_input_text(usr, "Enter a title for the ticket", "Rename Ticket", name) if(new_title) name = new_title @@ -486,70 +638,38 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) 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 + //TicketPanel() //we have to be here to do this -/datum/admin_help/tgui_fallback(payload) - if(..()) +//Kick ticket to next level +/datum/ticket/proc/Escalate() + if(tgui_alert(usr, "Really escalate this ticket to admins? No mentors will ever be able to interact with it again if you do.","Escalate",list("Yes","No")) != "Yes") + return + if (src.initiator == null) // You can't escalate a mentorhelp of someone who's logged out because it won't create the adminhelp properly + to_chat(usr, span_mentor_warning("Error: client not found, unable to escalate.")) return - TicketPanelLegacy() + level = level + 1 -/datum/admin_help/tgui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "AdminTicketPanel", "Ticket #[id] - [LinkedReplyName("\ref[src]")]") - ui.open() - -/datum/admin_help/tgui_state(mob/user) - return GLOB.tgui_admin_state - -/datum/admin_help/tgui_data(mob/user) - var/list/data = list() - - data["id"] = id - - var/ref_src = "\ref[src]" - data["title"] = name - data["name"] = LinkedReplyName(ref_src) - - switch(state) - if(AHELP_ACTIVE) - data["state"] = "open" - if(AHELP_RESOLVED) - data["state"] = "resolved" - if(AHELP_CLOSED) - data["state"] = "closed" - else - data["state"] = "unknown" - - data["opened_at"] = (world.time - opened_at) - data["closed_at"] = (world.time - closed_at) - data["opened_at_date"] = gameTimestamp(wtime = opened_at) - data["closed_at_date"] = gameTimestamp(wtime = closed_at) - - data["actions"] = FullMonty(ref_src) - - data["log"] = _interactions - - return data - -/datum/admin_help/tgui_act(action, params) - if(..()) - return - switch(action) - if("retitle") - Retitle() - . = TRUE - if("reopen") - Reopen() - . = TRUE - if("legacy") - TicketPanelLegacy() - . = TRUE + message_mentors("[usr.ckey] escalated Ticket [TicketHref("#[id]")]") + log_admin("[key_name(usr)] escalated ticket [src.name]") + to_chat(src.initiator, span_mentor("[usr.ckey] escalated your ticket to admins.")) //Forwarded action from admin/Topic -/datum/admin_help/proc/Action(action) - testing("Ahelp action: [action]") +/datum/ticket/proc/Action(action) + //testing("Ticket action: [action]") + + // Actions everyone can do + switch(level) + if(0) + if(!check_rights_for(usr.client, (R_ADMIN|R_SERVER|R_MOD|R_MENTOR))) + return + if(1) + if(!check_rights_for(usr.client, (R_ADMIN|R_SERVER|R_MOD))) + return + + perform_action(action) + +/datum/ticket/proc/perform_action(action) switch(action) if("ticket") TicketPanel() @@ -558,7 +678,11 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) if("reject") Reject() if("reply") - usr.client.cmd_ahelp_reply(initiator) + switch(level) + if(0) + usr.client.cmd_mhelp_reply(initiator) + if(1) + usr.client.cmd_ahelp_reply(initiator) if("icissue") ICIssue() if("close") @@ -569,91 +693,31 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) HandleIssue() if("reopen") Reopen() + if("escalate") + Escalate() // // TICKET STATCLICK // -/obj/effect/statclick/ahelp - var/datum/admin_help/ahelp_datum +/obj/effect/statclick/ticket + var/datum/ticket/ticket_datum -/obj/effect/statclick/ahelp/Initialize(mapload, text, target) +INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket) +/obj/effect/statclick/ticket/Initialize(mapload, datum/ticket/T) + ticket_datum = T . = ..() - ahelp_datum = target -/obj/effect/statclick/ahelp/update() - return ..(ahelp_datum.name) +/obj/effect/statclick/ticket/update() + return ..(ticket_datum.name) -/obj/effect/statclick/ahelp/Click() - ahelp_datum.TicketPanel() +/obj/effect/statclick/ticket/Click() + ticket_datum.TicketPanel() -/obj/effect/statclick/ahelp/Destroy() - ahelp_datum = null +/obj/effect/statclick/ticket/Destroy() + ticket_datum = null return ..() -// -// CLIENT PROCS -// - -/client/verb/adminhelp(msg as text) - set category = "Admin" - set name = "Adminhelp" - - //handle muting and automuting - if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, span_danger("Error: Admin-PM: You cannot send adminhelps (Muted).")) - return - if(handle_spam_prevention(msg,MUTE_ADMINHELP)) - return - - if(!msg) - return - - //remove out adminhelp verb temporarily to prevent spamming of admins. - remove_verb(src, /client/verb/adminhelp) - spawn(1200) - add_verb(src, /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) - var/input = tgui_alert(usr, "You already have a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) - if(!input) - return - if(input == "Yes") - if(current_ticket) - current_ticket.MessageNoRecipient(msg) - to_chat(usr, span_admin_pm_notice("PM to-" + span_bold("Admins") + ": [msg]")) - return - else - to_chat(usr, span_warning("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.Misc" - - if(!check_rights(R_ADMIN|R_MOD|R_DEBUG|R_EVENT, TRUE)) - return - - var/browse_to - - switch(tgui_input_list(usr, "Display which ticket list?", "List Choice", 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 // @@ -671,10 +735,10 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) 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 + var/datum/ticket/T = GLOB.tickets.CKey2ActiveTicket(what) + if(T) + T.AddInteraction(message) + return T // // HELPER PROCS diff --git a/code/modules/tickets/tickets_player_ui.dm b/code/modules/tickets/tickets_player_ui.dm new file mode 100644 index 0000000000..4e70f4df19 --- /dev/null +++ b/code/modules/tickets/tickets_player_ui.dm @@ -0,0 +1,52 @@ +// +//PLAYERSIDE TICKET UI +// + +/datum/ticket_chat + var/datum/ticket/T + +/datum/ticket_chat/tgui_interact(mob/user, datum/tgui/ui) + return // Remove this line to enable player-side ticket ui + //ui = SStgui.try_update_ui(user, src, ui) + //if(!ui) + // ui = new(user, src, "TicketChat", "Ticket #[T.id] - [T.LinkedReplyName("\ref[T]")]") + // ui.open() + // user.clear_alert("open ticket") + +/datum/ticket_chat/tgui_close(mob/user) + . = ..() + return // Remove this line to enable player-side ticket ui + //if(user.client.current_ticket) + // user.throw_alert("open ticket", /obj/screen/alert/open_ticket) + +/datum/ticket_chat/tgui_state(mob/user) + return GLOB.tgui_admin_state // Remove this line to enable player-side ticket ui + //return GLOB.tgui_ticket_state + +/datum/ticket_chat/tgui_data(mob/user) + var/list/data = list() + + data["id"] = T.id + + data["level"] = T.level + // data["handler"] = T.handler // Uncomment this line to enable player-side ticket ui + + // data["log"] = T._interactions // Uncomment this line to enable player-side ticket ui + + return data + +/datum/ticket_chat/tgui_act(action, params, datum/tgui/ui) + if(..()) + return + switch(action) + if("send_msg") + if(!params["msg"]) + return + + switch(T.level) + if (0) + ui.user.client.cmd_mentor_pm(ui.user.client, sanitize(params["msg"]), T) + if (1) + ui.user.client.cmd_admin_pm(ui.user.client, sanitize(params["msg"]), T) + + . = TRUE diff --git a/code/modules/tickets/tickets_ui.dm b/code/modules/tickets/tickets_ui.dm new file mode 100644 index 0000000000..a41dd7398a --- /dev/null +++ b/code/modules/tickets/tickets_ui.dm @@ -0,0 +1,334 @@ +// +//TICKET MANAGER +// + +/datum/tickets/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TicketsPanel", "Tickets") + ui.open() + +/datum/tickets/tgui_state(mob/user) + return GLOB.tgui_mentor_state + +/datum/tickets/proc/get_ticket_state(state) + var/ticket_state + switch(state) + if(AHELP_ACTIVE) + ticket_state = "open" + // TODO: Mentor tickets cannot be resolved + if(AHELP_RESOLVED) + ticket_state = "resolved" + if(AHELP_CLOSED) + ticket_state = "closed" + else + ticket_state = "unknown" + + return ticket_state + +/datum/tickets/tgui_data(mob/user) + var/list/data = list() + var/list/tickets = list() + + var/selected_ticket = null + + if(user.client.selected_ticket) + var/datum/ticket/T = user.client.selected_ticket + if(check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) || (check_rights_for(user.client, R_MENTOR) && T.level < 1)) + selected_ticket = list( + "id" = T.id, + "name" = T.LinkedReplyName(), + "state" = get_ticket_state(T.state), + "level" = T.level, + "handler" = T.handler, + "opened_at" = (world.time - T.opened_at), + "closed_at" = (world.time - T.closed_at), + "opened_at_date" = gameTimestamp(wtime = T.opened_at), + "closed_at_date" = gameTimestamp(wtime = T.closed_at), + "actions" = check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) ? T.FullMonty(,TRUE) : T.FullMonty(), + "log" = T._interactions, + ) + + for(var/datum/ticket/T as anything in GLOB.tickets.active_tickets) + if(check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) || (check_rights_for(user.client, R_MENTOR) && T.level < 1)) + tickets.Add(list(list( + "id" = T.id, + "name" = T.initiator_key_name, + "state" = get_ticket_state(T.state), + "level" = T.level, + "handler" = T.handler, + "opened_at" = (world.time - T.opened_at), + "closed_at" = (world.time - T.closed_at), + "opened_at_date" = gameTimestamp(wtime = T.opened_at), + "closed_at_date" = gameTimestamp(wtime = T.closed_at), + ))) + + for(var/datum/ticket/T as anything in GLOB.tickets.closed_tickets) + if(check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) || (check_rights_for(user.client, R_MENTOR) && T.level < 1)) + tickets.Add(list(list( + "id" = T.id, + "name" = T.initiator_key_name, + "state" = get_ticket_state(T.state), + "level" = T.level, + "handler" = T.handler, + "opened_at" = (world.time - T.opened_at), + "closed_at" = (world.time - T.closed_at), + "opened_at_date" = gameTimestamp(wtime = T.opened_at), + "closed_at_date" = gameTimestamp(wtime = T.closed_at), + ))) + + for(var/datum/ticket/T as anything in GLOB.tickets.resolved_tickets) + if(check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) || (check_rights_for(user.client, R_MENTOR) && T.level < 1)) + tickets.Add(list(list( + "id" = T.id, + "name" = T.initiator_key_name, + "state" = get_ticket_state(T.state), + "level" = T.level, + "handler" = T.handler, + "opened_at" = (world.time - T.opened_at), + "closed_at" = (world.time - T.closed_at), + "opened_at_date" = gameTimestamp(wtime = T.opened_at), + "closed_at_date" = gameTimestamp(wtime = T.closed_at), + ))) + data["tickets"] = tickets + data["is_admin"] = check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) + data["selected_ticket"] = selected_ticket + + return data + +/datum/tickets/tgui_act(action, params, datum/tgui/ui) + if(..()) + return + switch(action) + if("legacy") + var/choice = tgui_input_list(ui.user, "Which tickets do you want to list?", "Tickets", list("Active", "Closed", "Resolved")) + TicketListLegacy(ui.user, choice) + . = TRUE + if("new_ticket") + var/list/ckeys = list() + for(var/client/C in GLOB.clients) + ckeys += C.key + + var/ckey = lowertext(tgui_input_list(ui.user, "Please select the ckey of the user.", "Select CKEY", ckeys)) + if(!ckey) + return + + var/client/player + for(var/client/C in GLOB.clients) + if(C.ckey == ckey) + player = C + + if(!player) + to_chat(ui.user, span_warning("Ckey ([ckey]) not online.")) + return + + var/ticket_text = tgui_input_text(ui.user, "What should the initial text be?", "New Ticket") + if(!ticket_text) + to_chat(ui.user, span_warning("Ticket message cannot be empty.")) + return + + var/level = tgui_alert(ui.user, "Is this ticket Admin-Level or Mentor-Level?", "Ticket Level", list("Admin", "Mentor")) + if(!level) + return + + feedback_add_details("admin_verb","Admincreatedticket") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + if(player.current_ticket) + var/input = tgui_alert(ui.user, "The player already has a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) + if(!input) + return + if(input == "Yes") + if(player.current_ticket) + player.current_ticket.MessageNoRecipient(ticket_text) + to_chat(ui.user, span_adminnotice("PM to-" + span_bold("Admins") + ": [ticket_text]")) + return + else + to_chat(ui.user, span_warning("Ticket not found, creating new one...")) + else + player.current_ticket.AddInteraction("[key_name_admin(ui.user)] opened a new ticket.") + player.current_ticket.Close() + + // Create a new ticket and handle it. You created it afterall! + var/datum/ticket/T = new /datum/ticket(ticket_text, player, TRUE, level) + if(level == "Admin") + T.level = 1 + else + T.level = 0 + T.HandleIssue() + switch(T.level) + if (0) + ui.user.client.cmd_mentor_pm(player, ticket_text, T) + if (1) + ui.user.client.cmd_admin_pm(player, ticket_text, T) + . = TRUE + if("pick_ticket") + var/datum/ticket/T = ID2Ticket(params["ticket_id"]) + ui.user.client.selected_ticket = T + . = TRUE + if("retitle_ticket") + ui.user.client.selected_ticket.Retitle() + . = TRUE + if("reopen_ticket") + ui.user.client.selected_ticket.Reopen() + . = TRUE + if("undock_ticket") + ui.user.client.selected_ticket.tgui_interact(ui.user) + ui.user.client.selected_ticket = null + . = TRUE + if("send_msg") + if(!params["msg"]) + return + + switch(ui.user.client.selected_ticket.level) + if (0) + ui.user.client.cmd_mentor_pm(ui.user.client.selected_ticket.initiator, sanitize(params["msg"]), ui.user.client.selected_ticket) + if (1) + ui.user.client.cmd_admin_pm(ui.user.client.selected_ticket.initiator, sanitize(params["msg"]), ui.user.client.selected_ticket) + . = TRUE + +/datum/tickets/tgui_fallback(payload, user) + if(..()) + return + + var/choice = tgui_input_list(user, "Which tickets do you want to list?", "Tickets", list("Active", "Closed", "Resolved")) + + TicketListLegacy(user, choice) + +// +//TICKET DATUM +// + +/datum/ticket/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Ticket", "Ticket #[id] - [LinkedReplyName("\ref[src]")]") + ui.open() + +/datum/ticket/tgui_state(mob/user) + return GLOB.tgui_mentor_state + +/datum/ticket/tgui_data(mob/user) + var/list/data = list() + + data["id"] = id + + var/ref_src = "\ref[src]" + data["title"] = name + data["name"] = LinkedReplyName(ref_src) + data["ticket_ref"] = ref_src + + switch(state) + if(AHELP_ACTIVE) + data["state"] = "open" + // TODO: Mentor tickets cannot be resolved + if(AHELP_RESOLVED) + data["state"] = "resolved" + if(AHELP_CLOSED) + data["state"] = "closed" + else + data["state"] = "unknown" + + data["level"] = level + data["handler"] = handler + + data["opened_at"] = (world.time - opened_at) + data["closed_at"] = (world.time - closed_at) + data["opened_at_date"] = gameTimestamp(wtime = opened_at) + data["closed_at_date"] = gameTimestamp(wtime = closed_at) + + data["actions"] = check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) ? FullMonty(ref_src, TRUE) : FullMonty(ref_src) + + data["log"] = _interactions + + return data + +/datum/ticket/tgui_act(action, params, datum/tgui/ui) + if(..()) + return + switch(action) + if("retitle") + Retitle() + . = TRUE + if("reopen") + Reopen() + . = TRUE + if("legacy") + TicketPanelLegacy(ui.user) + . = TRUE + if("send_msg") + if(!params["msg"] || !params["ticket_ref"]) + return + + var/datum/ticket/T = locate(params["ticket_ref"]) + + switch(level) + if (0) + ui.user.client.cmd_mentor_pm(T.initiator, sanitize(params["msg"]), T) + if (1) + ui.user.client.cmd_admin_pm(T.initiator, sanitize(params["msg"]), T) + + . = TRUE + +/datum/ticket/tgui_fallback(payload, user) + if(..()) + return + + TicketPanelLegacy(user) + +/datum/ticket/proc/TicketPanelLegacy(mob/user) + var/list/dat = list("Ticket #[id]") + var/ref_src = "\ref[src]" + dat += "

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

" + dat += "State: " + switch(state) + if(AHELP_ACTIVE) + dat += span_red("OPEN") + if(AHELP_RESOLVED) + dat += span_green("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 += span_bold("Actions:") + " [check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) ? FullMonty(ref_src, TRUE) : FullMonty(ref_src)]
" + else + dat += span_bold("DISCONNECTED") + "[GLOB.TAB][ClosureLinks(ref_src)]
" + dat += "
Log:

" + for(var/I in _interactions) + dat += "[I]
" + dat += "" + user << browse(dat.Join(), "window=ahelp[id];size=620x480") + +/datum/tickets/proc/TicketListLegacy(mob/user, state) + var/list/dat = list("[state] Tickets") + var/tickets_found = 0 + + if(state == "Active") + for(var/datum/ticket/T as anything in GLOB.tickets.active_tickets) + if(!check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) && T.level > 0) + continue + dat += "[T.level == 0 ? "Mentorhelp" : "Adminhelp"] - [T.TicketHref("#[T.id] - [T.initiator_ckey]: [T.name]")]" + tickets_found++ + else if(state == "Closed") + for(var/datum/ticket/T as anything in GLOB.tickets.closed_tickets) + if(!check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) && T.level > 0) + continue + dat += "[T.level == 0 ? "Mentorhelp" : "Adminhelp"] - [T.TicketHref("#[T.id] - [T.initiator_ckey]: [T.name]")]" + tickets_found++ + else if(state == "Resolved") + for(var/datum/ticket/T as anything in GLOB.tickets.resolved_tickets) + if(!check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) && T.level > 0) + continue + dat += "[T.level == 0 ? "Mentorhelp" : "Adminhelp"] - [T.TicketHref("#[T.id] - [T.initiator_ckey]: [T.name]")]" + tickets_found++ + + if(tickets_found == 0) + dat += "No [state] tickets found." + dat += "" + user << browse(dat.Join(), "window=ahelp-list;size=250x350") diff --git a/config/example/config.txt b/config/example/config.txt index 9f7b48cc7b..a21581316c 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -592,3 +592,12 @@ JUKEBOX_TRACK_FILES config/jukebox.json ## Uncomment to limit resize caps depending on sprite icon height #PIXEL_SIZE_LIMIT + +# This is the tgs4 channel tag, for discord chatbots used in TGS +#AHELP_CHANNEL_TAG admin + +# Turn this off if you don't want the TGS bot sending you messages whenever an ahelp ticket is created. +#DISCORD_AHELPS_DISABLED + +#Turn this on if you want all admin-PMs to go to be sent to discord, and not only the first message of a ticket. +#DISCORD_AHELPS_ALL diff --git a/config/example/mentors.txt b/config/example/mentors.txt deleted file mode 100644 index 5dd8ccc673..0000000000 --- a/config/example/mentors.txt +++ /dev/null @@ -1,4 +0,0 @@ -; just add the ckey (lowercase) of every mentor on a separate line -; lines starting with ; are comments and will be ignored - -; not_a_user diff --git a/html/statbrowser.js b/html/statbrowser.js index 3d671c81c3..64c86eb5d1 100644 --- a/html/statbrowser.js +++ b/html/statbrowser.js @@ -1066,6 +1066,11 @@ Byond.subscribeTo('add_admin_tabs', function (ht) { addPermanentTab("Tickets"); }); +Byond.subscribeTo('add_tickets_tabs', function (ht) { + href_token = ht; + addPermanentTab("Tickets"); +}); + Byond.subscribeTo('update_examine', function (payload) { examine = payload.EX; if (examine.length > 0 && !verb_tabs.includes("Examine")) { diff --git a/icons/logo.dmi b/icons/logo.dmi new file mode 100644 index 0000000000..f7af2aa2a3 Binary files /dev/null and b/icons/logo.dmi differ diff --git a/icons/virgoicon_32.png b/icons/virgoicon_32.png new file mode 100644 index 0000000000..eeacf2be89 Binary files /dev/null and b/icons/virgoicon_32.png differ diff --git a/tgui/packages/tgui/interfaces/AdminTicketPanel.tsx b/tgui/packages/tgui/interfaces/AdminTicketPanel.tsx deleted file mode 100644 index 5428b9f78b..0000000000 --- a/tgui/packages/tgui/interfaces/AdminTicketPanel.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint react/no-danger: "off" */ -import { useBackend } from 'tgui/backend'; -import { Window } from 'tgui/layouts'; -import { Button, LabeledList, Section, Stack } from 'tgui-core/components'; -import { round, toFixed } from 'tgui-core/math'; - -const State = { - open: 'Open', - resolved: 'Resolved', - closed: 'Closed', - unknown: 'Unknown', -}; - -type Data = { - id: number; - title: string; - name: string; - state: string; - opened_at: number; - closed_at: number; - opened_at_date: string; - closed_at_date: string; - actions: string; - log: string[]; -}; - -export const AdminTicketPanel = (props) => { - const { act, data } = useBackend(); - const { - id, - title, - name, - state, - opened_at, - closed_at, - opened_at_date, - closed_at_date, - actions, - log, - } = data; - return ( - - -
- - - - - - - - } - > - - - #{id}:
- - {State[state]} - {State[state] === State.open ? ( - - {opened_at_date + - ' (' + - toFixed(round((opened_at / 600) * 10, 0) / 10, 1) + - ' minutes ago.)'} - - ) : ( - - {closed_at_date + - ' (' + - toFixed(round((closed_at / 600) * 10, 0) / 10, 1) + - ' minutes ago.)'} - - - )} - -
- - - {Object.keys(log).map((L, i) => ( -
- ))} - - -
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/MentorTicketPanel.tsx b/tgui/packages/tgui/interfaces/MentorTicketPanel.tsx deleted file mode 100644 index d803d39ce9..0000000000 --- a/tgui/packages/tgui/interfaces/MentorTicketPanel.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint react/no-danger: "off" */ -import { useBackend } from 'tgui/backend'; -import { Window } from 'tgui/layouts'; -import { Button, LabeledList, Section, Stack } from 'tgui-core/components'; -import { round, toFixed } from 'tgui-core/math'; - -const State = { - open: 'Open', - resolved: 'Resolved', - unknown: 'Unknown', -}; - -type Data = { - id: number; - title: string; - name: string; - state: string; - opened_at: number; - closed_at: number; - opened_at_date: string; - closed_at_date: string; - actions: string; - log: string[]; -}; - -export const MentorTicketPanel = (props) => { - const { act, data } = useBackend(); - const { - id, - name, - state, - opened_at, - closed_at, - opened_at_date, - closed_at_date, - actions, - log, - } = data; - return ( - - -
- - - - - - - - } - > - - - #{id}:
- - {State[state]} - {State[state] === State.open ? ( - - {opened_at_date + - ' (' + - toFixed(round((opened_at / 600) * 10, 0) / 10, 1) + - ' minutes ago.)'} - - ) : ( - - {closed_at_date + - ' (' + - toFixed(round((closed_at / 600) * 10, 0) / 10, 1) + - ' minutes ago.)'} - - - )} - -
- - - {Object.keys(log).map((L, i) => ( -
- ))} - - -
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/Ticket.tsx b/tgui/packages/tgui/interfaces/Ticket.tsx new file mode 100644 index 0000000000..c3e3a642fa --- /dev/null +++ b/tgui/packages/tgui/interfaces/Ticket.tsx @@ -0,0 +1,207 @@ +/* eslint react/no-danger: "off" */ +import { type RefObject, useEffect, useRef, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { + Box, + Button, + Divider, + Input, + LabeledList, + Section, + Stack, +} from 'tgui-core/components'; +import { KEY } from 'tgui-core/keys'; +import { round, toFixed } from 'tgui-core/math'; + +const Level = { + 0: 'Mentorhelp', + 1: 'Adminhelp', + 2: 'GM Request', +}; + +const LevelColor = { + 0: 'green', + 1: 'red', + 2: 'pink', +}; + +const Tag = { + example: 'Example', +}; + +const State = { + open: 'Open', + resolved: 'Resolved', + closed: 'Closed', + unknown: 'Unknown', +}; + +type Data = { + id: number; + title: string; + name: string; + ticket_ref: string; + state: string; + level: number; + handler: string; + opened_at: number; + closed_at: number; + opened_at_date: string; + closed_at_date: string; + actions: string; + log: string[]; +}; + +window.addEventListener('keydown', (event) => { + console.log(event); +}); + +export const Ticket = (props) => { + const { act, data } = useBackend(); + const [ticketChat, setTicketChat] = useState(''); + + const messagesEndRef: RefObject = useRef(null); + + useEffect(() => { + const scroll = messagesEndRef.current; + if (scroll) { + scroll.scrollTop = scroll.scrollHeight; + } + }, []); + + useEffect(() => { + const scroll = messagesEndRef.current; + if (scroll) { + const height = scroll.scrollHeight; + const bottom = scroll.scrollTop + scroll.offsetHeight; + const scrollTracking = Math.abs(height - bottom) < 24; + if (scrollTracking) { + scroll.scrollTop = scroll.scrollHeight; + } + } + }); + + const { + id, + name, + ticket_ref, + state, + level, + handler, + opened_at, + closed_at, + opened_at_date, + closed_at_date, + actions, + log, + } = data; + return ( + + + + +
+ + + + + } + > + + + #{id}:
+ + {Level[level]} + + {State[state]} + + {handler} + {State[state] === State.open ? ( + + {opened_at_date + + ' (' + + toFixed(round((opened_at / 600) * 10, 0) / 10, 1) + + ' minutes ago.)'} + + ) : ( + + {closed_at_date + + ' (' + + toFixed(round((closed_at / 600) * 10, 0) / 10, 1) + + ' minutes ago.)'} + + + )} + +
+ + + +
+ +
+ +
+ + + {Object.keys(log) + .slice(0) + .map((L, i) => ( +
+ ))} + + +
+
+ +
+ + + setTicketChat(value)} + onKeyDown={(e) => { + if (KEY.Enter === e.key) { + act('send_msg', { + msg: ticketChat, + ticket_ref: ticket_ref, + }); + setTicketChat(''); + } + }} + /> + + + + + +
+
+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/TicketChat.tsx b/tgui/packages/tgui/interfaces/TicketChat.tsx new file mode 100644 index 0000000000..ab757f0f37 --- /dev/null +++ b/tgui/packages/tgui/interfaces/TicketChat.tsx @@ -0,0 +1,139 @@ +/* eslint react/no-danger: "off" */ +import { type RefObject, useEffect, useRef, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { + Button, + Divider, + Input, + LabeledList, + Section, + Stack, +} from 'tgui-core/components'; +import { KEY } from 'tgui-core/keys'; + +const Level = { + 0: 'Mentorhelp', + 1: 'Adminhelp', + 2: 'GM Request', +}; + +const LevelColor = { + 0: 'green', + 1: 'red', + 2: 'pink', +}; + +const Tag = { + example: 'Example', +}; + +const State = { + open: 'Open', + resolved: 'Resolved', + closed: 'Closed', + unknown: 'Unknown', +}; + +type Data = { + id: number; + level: number; + handler: string; + log: string[]; +}; + +export const TicketChat = (props) => { + const { act, data } = useBackend(); + const [ticketChat, setTicketChat] = useState(''); + const { id, level, handler, log } = data; + + const messagesEndRef: RefObject = useRef(null); + + useEffect(() => { + const scroll = messagesEndRef.current; + if (scroll) { + scroll.scrollTop = scroll.scrollHeight; + } + }, []); + + useEffect(() => { + const scroll = messagesEndRef.current; + if (scroll) { + const height = scroll.scrollHeight; + const bottom = scroll.scrollTop + scroll.offsetHeight; + const scrollTracking = Math.abs(height - bottom) < 24; + if (scrollTracking) { + scroll.scrollTop = scroll.scrollHeight; + } + } + }); + + return ( + + + + +
{Level[level]} + } + > + + {handler} + + +
+ +
+ +
+ + + {Object.keys(log) + .slice(0) + .map((L, i) => ( +
+ ))} + + +
+
+ + + setTicketChat(value)} + onKeyDown={(e) => { + if (KEY.Enter === e.key) { + act('send_msg', { msg: ticketChat }); + setTicketChat(''); + } + }} + /> + + + + + +
+
+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/TicketsPanel.tsx b/tgui/packages/tgui/interfaces/TicketsPanel.tsx new file mode 100644 index 0000000000..5fb8843015 --- /dev/null +++ b/tgui/packages/tgui/interfaces/TicketsPanel.tsx @@ -0,0 +1,366 @@ +/* eslint react/no-danger: "off" */ +import { type RefObject, useEffect, useRef, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { + Box, + Button, + Divider, + Dropdown, + Icon, + Input, + LabeledList, + Section, + Stack, + Tabs, +} from 'tgui-core/components'; +import { KEY } from 'tgui-core/keys'; +import { round, toFixed } from 'tgui-core/math'; +import type { BooleanLike } from 'tgui-core/react'; + +const AdminLevel = { + 0: 'Mentor', + 1: 'Admin', + 2: 'All Levels', +}; + +const MentorLevel = { + 0: 'Mentor', +}; + +const LevelColor = { + 0: 'green', + 1: 'red', + 2: 'pink', +}; + +const Tag = { + example: 'Example', +}; + +const State = { + open: 'Open', + resolved: 'Resolved', + closed: 'Closed', + unknown: 'Unknown', + all: 'All States', +}; + +const StateColor = { + open: 'white', + resolved: 'green', + closed: 'grey', + unknown: 'orange', +}; + +type Data = { + tickets: Ticket[]; + + selected_ticket: Ticket; + is_admin: BooleanLike; +}; + +type Ticket = { + id: number; + title: string; + name: string; + state: string; + level: number; + handler: string; + opened_at: number; + closed_at: number; + opened_at_date: string; + closed_at_date: string; + actions: string; + log: string[]; + + ref: string; + selected: BooleanLike; +}; + +const getFilteredTickets = ( + tickets: Ticket[], + state: string, + level: number, +): Ticket[] => { + const result: Ticket[] = []; + + tickets.forEach((t) => { + if ( + (t.state === state || state === 'all') && + (t.level === level || level === 2) + ) { + result.push(t); + } + }); + + return result; +}; + +export const TicketsPanel = (props) => { + const { act, data } = useBackend(); + const { tickets, selected_ticket, is_admin } = data; + + const [stateFilter, setStateFilter] = useState('open'); + const [levelFilter, setLevelFilter] = useState(2); + + const [ticketChat, setTicketChat] = useState(''); + + const messagesEndRef: RefObject = useRef(null); + + useEffect(() => { + const scroll = messagesEndRef.current; + if (scroll) { + scroll.scrollTop = scroll.scrollHeight; + } + }, []); + + useEffect(() => { + const scroll = messagesEndRef.current; + if (scroll) { + const height = scroll.scrollHeight; + const bottom = scroll.scrollTop + scroll.offsetHeight; + const scrollTracking = Math.abs(height - bottom) < 24; + if (scrollTracking) { + scroll.scrollTop = scroll.scrollHeight; + } + } + }); + + const availableLevel = is_admin ? AdminLevel : MentorLevel; + + const filtered_tickets = getFilteredTickets( + tickets, + stateFilter, + levelFilter, + ); + return ( + + + + +
+ + setStateFilter( + Object.keys(State)[Object.values(State).indexOf(val)], + ) + } + /> + + + setLevelFilter(Object.values(availableLevel).indexOf(val)) + } + /> +
+
+ + act('new_ticket')}> + New Ticket + + + + {filtered_tickets.map((ticket) => ( + act('pick_ticket', { ticket_id: ticket.id })} + > + + + + {ticket.name} + + + State: {State[ticket.state]} | Assignee: + {ticket.handler} + + + + ))} + +
+
+ + {(selected_ticket && ( + + +
+ + + + + + } + > + + + #{selected_ticket.id}: +
+ + + {availableLevel[selected_ticket.level]} + + + {State[selected_ticket.state]} + + + {selected_ticket.handler} + + {State[selected_ticket.state] === State.open ? ( + + {selected_ticket.opened_at_date + + ' (' + + toFixed( + round((selected_ticket.opened_at / 600) * 10, 0) / + 10, + 1, + ) + + ' minutes ago.)'} + + ) : ( + + {selected_ticket.closed_at_date + + ' (' + + toFixed( + round((selected_ticket.closed_at / 600) * 10, 0) / + 10, + 1, + ) + + ' minutes ago.)'} + + + )} + +
+ + + +
+ +
+ +
+ + + {Object.keys(selected_ticket.log) + .slice(0) + .map((L, i) => ( +
+ ))} + + +
+
+ +
+ + + setTicketChat(value)} + onKeyDown={(e) => { + if (KEY.Enter === e.key) { + act('send_msg', { msg: ticketChat }); + setTicketChat(''); + } + }} + /> + + + + + +
+
+
+ )) || ( +
+ + + + + } + > + Please select a ticket on the left to view its details. +
+ )} +
+
+
+
+ ); +}; diff --git a/tools/admin_rank_bitflag_calculator.py b/tools/admin_rank_bitflag_calculator.py index 758031cfd0..d60af4d197 100644 --- a/tools/admin_rank_bitflag_calculator.py +++ b/tools/admin_rank_bitflag_calculator.py @@ -18,8 +18,9 @@ bitflags = { "R_MOD": 1<<13, "R_EVENT": 1<<14, "R_HOST": 1<<15, + "R_MENTOR": 1<<16, "-------------": 0, - "EVERYTHING": (1<<16)-1 + "EVERYTHING": (1<<17)-1 } class BitflagCalculator: diff --git a/vorestation.dme b/vorestation.dme index a065a526ed..0b6e8980fc 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -1971,8 +1971,6 @@ #include "code\modules\admin\secrets\random_events\trigger_cordical_borer_infestation.dm" #include "code\modules\admin\secrets\random_events\trigger_xenomorph_infestation.dm" #include "code\modules\admin\verb_datums\_admin_verb_datum.dm" -#include "code\modules\admin\verbs\adminhelp.dm" -#include "code\modules\admin\verbs\adminhelp_vr.dm" #include "code\modules\admin\verbs\adminjump.dm" #include "code\modules\admin\verbs\adminpm.dm" #include "code\modules\admin\verbs\adminsay.dm" @@ -2986,8 +2984,6 @@ #include "code\modules\media\media_tracks.dm" #include "code\modules\media\mediamanager.dm" #include "code\modules\media\walkpod.dm" -#include "code\modules\mentor\mentor.dm" -#include "code\modules\mentor\mentorhelp.dm" #include "code\modules\metric\activity.dm" #include "code\modules\metric\count.dm" #include "code\modules\metric\department.dm" @@ -4374,6 +4370,7 @@ #include "code\modules\tgui\states\observer.dm" #include "code\modules\tgui\states\physical.dm" #include "code\modules\tgui\states\self.dm" +#include "code\modules\tgui\states\ticket.dm" #include "code\modules\tgui\states\vorepanel_vr.dm" #include "code\modules\tgui\states\zlevel.dm" #include "code\modules\tgui_input\alert.dm" @@ -4389,6 +4386,10 @@ #include "code\modules\tgui_panel\external.dm" #include "code\modules\tgui_panel\telemetry.dm" #include "code\modules\tgui_panel\tgui_panel.dm" +#include "code\modules\tickets\procs.dm" +#include "code\modules\tickets\tickets.dm" +#include "code\modules\tickets\tickets_player_ui.dm" +#include "code\modules\tickets\tickets_ui.dm" #include "code\modules\tooltip\tooltip.dm" #include "code\modules\turbolift\_turbolift.dm" #include "code\modules\turbolift\turbolift.dm"