Ports ticket system overhaul from downstream (#17063)

* Ports ticket system overhaul from downstream

* Various fixes

* fixes

* Update tickets.dm

* Update tickets.dm

* Update ticket.dm

* Refactor

---------

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
Selis
2025-06-28 02:28:15 +02:00
committed by GitHub
co-authored by Kashargul
parent a294b8c549
commit f2b86cb5e3
46 changed files with 1983 additions and 1765 deletions
+3 -1
View File
@@ -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"
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -204,8 +204,8 @@
// Mentor pm filter
#define span_mentor(str) ("<span class='mentor'>" + str + "</span>")
#define span_mentor_pm_notice(str) ("<span class='mentor notice'>" + str + "</span>")
#define span_mentor_pm_warning(str) ("<span class='mentor warning'>" + str + "</span>")
#define span_mentor_notice(str) ("<span class='mentor notice'>" + str + "</span>")
#define span_mentor_warning(str) ("<span class='mentor warning'>" + str + "</span>")
/* Adminchat */
// All of those have their own filter
+1
View File
@@ -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(
-2
View File
@@ -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
+1
View File
@@ -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) ).
+18 -4
View File
@@ -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
@@ -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
+5 -4
View File
@@ -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)
-35
View File
@@ -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 = ""
+2
View File
@@ -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)
-183
View File
@@ -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])<br>"
output += span_bold("Offense:") + "[N.body]<br>"
output += "<small>Occurred at [time2text(N.date,"MM/DD hh:mm:ss")]</small><br>"
output += "<small>authored by <i>[N.author]</i></small><br>"
output += " <a href='byond://?src=\ref[report_topic_handler];client=\ref[src];[HrefToken()];action=remove;ID=[N.ID]'>Flag as Handled</a>"
if(src.key == N.author)
output += " <a href='byond://?src=\ref[report_topic_handler];client=\ref[src];[HrefToken()];action=edit;ID=[N.ID]'>Edit</a>"
output += "<br>"
output += "<br>"
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
-36
View File
@@ -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,
+15 -22
View File
@@ -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")
+2 -9
View File
@@ -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))
+16 -16
View File
@@ -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")
-39
View File
@@ -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
+6 -6
View File
@@ -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-<b>[key_name(src, recipient, 0)]</b>: [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, " ")
+24 -34
View File
@@ -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]")))
+84 -84
View File
@@ -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 <a href='byond://?_src_=vars;[HrefToken(TRUE)];datumrefresh=\ref[thing]'>VV window</a>")
log_admin("Admin [key_name(usr)] Showed [key_name(C)] a VV window of a [src]")
@@ -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"
+2 -12
View File
@@ -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
+8 -20
View File
@@ -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.")
-268
View File
@@ -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-<b>[src]</b> to-<b>[recipient]</b>: [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-<b><a href='byond://?mentorhelp_msg=\ref[src]'>[src]</a></b>: [msg]")))
to_chat(src, span_mentor(span_italics("Mentor-PM to-<b>[recipient]</b>: [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)
-507
View File
@@ -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 += "<A href='byond://?_src_=mentorholder;[HrefToken()];mhelp_tickets=[state]'>Refresh</A><br><br>"
for(var/datum/mentor_help/MH as anything in l2b)
dat += span_adminnotice(span_adminhelp("Ticket #[MH.id]") + " <A href='byond://?_src_=mentorholder;mhelp=\ref[MH];[HrefToken()];mhelp_action=ticket'>[MH.initiator_ckey]: [MH.name]</A>") + "<br>"
var/datum/browser/popup = new(usr, "mhelp_list[state]", "Mentor Help List", 600, 480)
popup.add_head_content("<title>[title]</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]"
. = " (<A href='byond://?_src_=mentorholder;mhelp=[ref_src];[HrefToken()];mhelp_action=resolve'>RSLVE</A>)"
//private
/datum/mentor_help/proc/LinkedReplyName(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
return "<A href='byond://?_src_=mentorholder;mhelp=[ref_src];[HrefToken()];mhelp_action=reply'>[initiator_ckey]</A>"
//private
/datum/mentor_help/proc/TicketHref(msg, ref_src, action = "ticket")
if(!ref_src)
ref_src = "\ref[src]"
return "<A href='byond://?_src_=mentorholder;mhelp=[ref_src];[HrefToken()];mhelp_action=[action]'>[msg]</A>"
//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("(<A href='byond://?_src_=mentorholder;mhelp=[ref_src];[HrefToken()];mhelp_action=escalate'>ESCALATE</A>) 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 += "<h4>Mentor Help Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
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 += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)"
if(closed_at)
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)"
dat += "<br><br>"
if(initiator)
dat += span_bold("Actions:") + " [Context(ref_src)]<br>"
else
dat += span_bold("DISCONNECTED") + "[GLOB.TAB][ClosureLinks(ref_src)]<br>"
dat += "<br>" + span_bold("Log:") + "<br><br>"
for(var/I in _interactions)
dat += "[I]<br>"
var/datum/browser/popup = new(usr, "mhelp[id]", "Mento Help [id]", 620, 480)
popup.add_head_content("<title>Ticket #[id]</title>")
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)
. += " (<A href='byond://?_src_=mentorholder;[HrefToken()];mhelp=[ref_src];mhelp_action=escalate'>ESCALATE</A>)"
//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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+13
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+295
View File
@@ -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-<b>[src]</b> to-<b>[recipient]</b>: [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-<b><a href='byond://?mentorhelp_msg=\ref[src]'>[src]</a></b>: [msg]")))
to_chat(src, span_mentor(span_italics("Mentor-PM to-<b>[recipient]</b>: [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)
@@ -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("<html><head><title>[title]</title></head>")
dat += "<A href='byond://?_src_=holder;[HrefToken()];ahelp_tickets=[state]'>Refresh</A><br><br>"
for(var/datum/admin_help/AH as anything in l2b)
dat += span_adminnotice(span_adminhelp("Ticket #[AH.id]") + ": <A href='byond://?_src_=holder;ahelp=\ref[AH];[HrefToken()];ahelp_action=ticket'>[AH.initiator_key_name]: [AH.name]</A>") + "<br>"
for(var/datum/ticket/T as anything in l2b)
dat += span_adminnotice(span_adminhelp("Ticket #[T.id]") + ": <A href='byond://?_src_=holder;ahelp=\ref[T];[HrefToken()];ahelp_action=ticket'>[T.initiator_key_name]: [T.name]</A>") + "<br>"
dat += "</html>"
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]"
. = " (<A href='byond://?_src_=holder;ahelp=[ref_src];[HrefToken(TRUE)];ahelp_action=reject'>REJT</A>)"
. += " (<A href='byond://?_src_=holder;ahelp=[ref_src];[HrefToken(TRUE)];ahelp_action=icissue'>IC</A>)"
. += " (<A href='byond://?_src_=holder;ahelp=[ref_src];[HrefToken(TRUE)];ahelp_action=close'>CLOSE</A>)"
. += " (<A href='byond://?_src_=holder;ahelp=[ref_src];[HrefToken(TRUE)];ahelp_action=resolve'>RSLVE</A>)"
. += " (<A href='byond://?_src_=holder;ahelp=[ref_src];[HrefToken(TRUE)];ahelp_action=handleissue'>HANDLE</A>)"
if(level == 1)
. = " (<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=reject'>REJT</A>)"
. += " (<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=icissue'>IC</A>)"
. += " (<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=close'>CLOSE</A>)"
. += " (<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=resolve'>RSLVE</A>)"
. += " (<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=handleissue'>HANDLE</A>)"
else
. = " (<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=resolve'>RSLVE</A>)"
. += " (<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=escalate'>ESCALATE</A>)"
//private
/datum/admin_help/proc/LinkedReplyName(ref_src)
/datum/ticket/proc/LinkedReplyName(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
return "<A href='byond://?_src_=holder;ahelp=[ref_src];[HrefToken()];ahelp_action=reply'>[initiator_key_name]</A>"
return "<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=reply'>[initiator_key_name]</A>"
//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 "<A href='byond://?_src_=holder;ahelp=[ref_src];[HrefToken()];ahelp_action=[action]'>[msg]</A>"
return "<A href='byond://?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=[action]'>[msg]</A>"
/*
var/chat_msg = span_notice("(<A href='byond://?_src_=holder;ahelp=[ref_src];[HrefToken()];ahelp_action=escalate'>ESCALATE</A>) 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("<html><head><title>Ticket #[id]</title></head>")
var/ref_src = "\ref[src]"
dat += "<h4>Admin Help Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
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 += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)"
if(closed_at)
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)"
dat += "<br><br>"
if(initiator)
dat += span_bold("Actions:") + " [FullMonty(ref_src)]<br>"
else
dat += span_bold("DISCONNECTED") + "[GLOB.TAB][ClosureLinks(ref_src)]<br>"
dat += "<br>" + span_bold("Log:") + "<br><br>"
for(var/I in _interactions)
dat += "[I]<br>"
dat += "</html>"
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
+52
View File
@@ -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
+334
View File
@@ -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("<html><head><title>Ticket #[id]</title></head>")
var/ref_src = "\ref[src]"
dat += "<h4>Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
dat += "<b>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 += "</b>[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 += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)"
if(closed_at)
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)"
dat += "<br><br>"
if(initiator)
dat += span_bold("Actions:") + " [check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) ? FullMonty(ref_src, TRUE) : FullMonty(ref_src)]<br>"
else
dat += span_bold("DISCONNECTED") + "[GLOB.TAB][ClosureLinks(ref_src)]<br>"
dat += "<br><b>Log:</b><br><br>"
for(var/I in _interactions)
dat += "[I]<br>"
dat += "</html>"
user << browse(dat.Join(), "window=ahelp[id];size=620x480")
/datum/tickets/proc/TicketListLegacy(mob/user, state)
var/list/dat = list("<html><head><title>[state] Tickets</title></head>")
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 += "</html>"
user << browse(dat.Join(), "window=ahelp-list;size=250x350")
+9
View File
@@ -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
-4
View File
@@ -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
+5
View File
@@ -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")) {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -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<Data>();
const {
id,
title,
name,
state,
opened_at,
closed_at,
opened_at_date,
closed_at_date,
actions,
log,
} = data;
return (
<Window width={900} height={600}>
<Window.Content scrollable>
<Section
title={'Ticket #' + id}
buttons={
<Stack>
<Stack.Item>
<Button icon="pen" onClick={() => act('retitle')}>
Rename Ticket
</Button>
</Stack.Item>
<Stack.Item>
<Button onClick={() => act('legacy')}>Legacy UI</Button>
</Stack.Item>
</Stack>
}
>
<LabeledList>
<LabeledList.Item label="Admin Help Ticket">
#{id}: <div dangerouslySetInnerHTML={{ __html: name }} />
</LabeledList.Item>
<LabeledList.Item label="State">{State[state]}</LabeledList.Item>
{State[state] === State.open ? (
<LabeledList.Item label="Opened At">
{opened_at_date +
' (' +
toFixed(round((opened_at / 600) * 10, 0) / 10, 1) +
' minutes ago.)'}
</LabeledList.Item>
) : (
<LabeledList.Item label="Closed At">
{closed_at_date +
' (' +
toFixed(round((closed_at / 600) * 10, 0) / 10, 1) +
' minutes ago.)'}
<Button onClick={() => act('reopen')}>Reopen</Button>
</LabeledList.Item>
)}
<LabeledList.Item label="Actions">
<div dangerouslySetInnerHTML={{ __html: actions }} />
</LabeledList.Item>
<LabeledList.Item label="Log">
{Object.keys(log).map((L, i) => (
<div key={i} dangerouslySetInnerHTML={{ __html: log[L] }} />
))}
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
@@ -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<Data>();
const {
id,
name,
state,
opened_at,
closed_at,
opened_at_date,
closed_at_date,
actions,
log,
} = data;
return (
<Window width={900} height={600}>
<Window.Content scrollable>
<Section
title={'Ticket #' + id}
buttons={
<Stack>
<Stack.Item>
<Button icon="arrow-up" onClick={() => act('escalate')}>
Escalate
</Button>
</Stack.Item>
<Stack.Item>
<Button onClick={() => act('legacy')}>Legacy UI</Button>
</Stack.Item>
</Stack>
}
>
<LabeledList>
<LabeledList.Item label="Mentor Help Ticket">
#{id}: <div dangerouslySetInnerHTML={{ __html: name }} />
</LabeledList.Item>
<LabeledList.Item label="State">{State[state]}</LabeledList.Item>
{State[state] === State.open ? (
<LabeledList.Item label="Opened At">
{opened_at_date +
' (' +
toFixed(round((opened_at / 600) * 10, 0) / 10, 1) +
' minutes ago.)'}
</LabeledList.Item>
) : (
<LabeledList.Item label="Closed At">
{closed_at_date +
' (' +
toFixed(round((closed_at / 600) * 10, 0) / 10, 1) +
' minutes ago.)'}
<Button onClick={() => act('reopen')}>Reopen</Button>
</LabeledList.Item>
)}
<LabeledList.Item label="Actions">
<div dangerouslySetInnerHTML={{ __html: actions }} />
</LabeledList.Item>
<LabeledList.Item label="Log">
{Object.keys(log).map((L, i) => (
<div key={i} dangerouslySetInnerHTML={{ __html: log[L] }} />
))}
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
+207
View File
@@ -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<Data>();
const [ticketChat, setTicketChat] = useState('');
const messagesEndRef: RefObject<HTMLDivElement | null> = 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 (
<Window width={900} height={600}>
<Window.Content>
<Stack fill vertical>
<Stack.Item>
<Section
title={'Ticket #' + id}
buttons={
<Box nowrap>
<Button icon="pen" onClick={() => act('retitle')}>
Rename Ticket
</Button>
<Button onClick={() => act('legacy')}>Legacy UI</Button>
<Button color={LevelColor[level]}>{Level[level]}</Button>
</Box>
}
>
<LabeledList>
<LabeledList.Item label="Ticket ID">
#{id}: <div dangerouslySetInnerHTML={{ __html: name }} />
</LabeledList.Item>
<LabeledList.Item label="Type">{Level[level]}</LabeledList.Item>
<LabeledList.Item label="State">
{State[state]}
</LabeledList.Item>
<LabeledList.Item label="Assignee">{handler}</LabeledList.Item>
{State[state] === State.open ? (
<LabeledList.Item label="Opened At">
{opened_at_date +
' (' +
toFixed(round((opened_at / 600) * 10, 0) / 10, 1) +
' minutes ago.)'}
</LabeledList.Item>
) : (
<LabeledList.Item label="Closed At">
{closed_at_date +
' (' +
toFixed(round((closed_at / 600) * 10, 0) / 10, 1) +
' minutes ago.)'}
<Button onClick={() => act('reopen')}>Reopen</Button>
</LabeledList.Item>
)}
<LabeledList.Item label="Actions">
<div dangerouslySetInnerHTML={{ __html: actions }} />
</LabeledList.Item>
<LabeledList.Item label="Log" />
</LabeledList>
</Section>
<Divider />
</Stack.Item>
<Stack.Item grow>
<Section scrollable ref={messagesEndRef} fill>
<Stack fill direction="column">
<Stack.Item grow>
{Object.keys(log)
.slice(0)
.map((L, i) => (
<div
key={i}
dangerouslySetInnerHTML={{ __html: log[L] }}
/>
))}
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item>
<Section fill>
<Stack fill>
<Stack.Item grow>
<Input
autoFocus
autoSelect
fluid
placeholder="Enter a message..."
value={ticketChat}
onChange={(value: string) => setTicketChat(value)}
onKeyDown={(e) => {
if (KEY.Enter === e.key) {
act('send_msg', {
msg: ticketChat,
ticket_ref: ticket_ref,
});
setTicketChat('');
}
}}
/>
</Stack.Item>
<Stack.Item>
<Button
onClick={() => {
act('send_msg', {
msg: ticketChat,
ticket_ref: ticket_ref,
});
setTicketChat('');
}}
>
Send
</Button>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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<Data>();
const [ticketChat, setTicketChat] = useState('');
const { id, level, handler, log } = data;
const messagesEndRef: RefObject<HTMLDivElement | null> = 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 (
<Window width={900} height={600}>
<Window.Content>
<Stack fill vertical>
<Stack.Item>
<Section
title={'Ticket #' + id}
buttons={
<Button color={LevelColor[level]}>{Level[level]}</Button>
}
>
<LabeledList>
<LabeledList.Item label="Assignee">{handler}</LabeledList.Item>
<LabeledList.Item label="Log" />
</LabeledList>
</Section>
<Divider />
</Stack.Item>
<Stack.Item grow>
<Section fill ref={messagesEndRef} scrollable>
<Stack fill direction="column">
<Stack.Item grow>
{Object.keys(log)
.slice(0)
.map((L, i) => (
<div
key={i}
dangerouslySetInnerHTML={{ __html: log[L] }}
/>
))}
</Stack.Item>
</Stack>
</Section>
<Section fill>
<Stack fill>
<Stack.Item grow>
<Input
autoFocus
autoSelect
fluid
placeholder="Enter a message..."
value={ticketChat}
onChange={(value: string) => setTicketChat(value)}
onKeyDown={(e) => {
if (KEY.Enter === e.key) {
act('send_msg', { msg: ticketChat });
setTicketChat('');
}
}}
/>
</Stack.Item>
<Stack.Item>
<Button
onClick={() => {
act('send_msg', { msg: ticketChat });
setTicketChat('');
}}
>
Send
</Button>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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<Data>();
const { tickets, selected_ticket, is_admin } = data;
const [stateFilter, setStateFilter] = useState('open');
const [levelFilter, setLevelFilter] = useState(2);
const [ticketChat, setTicketChat] = useState('');
const messagesEndRef: RefObject<HTMLDivElement | null> = 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 (
<Window width={1000} height={600}>
<Window.Content>
<Stack fill>
<Stack.Item shrink>
<Section title="Filter">
<Dropdown
width="100%"
maxHeight="160px"
options={Object.values(State)}
selected={State[stateFilter]}
onSelected={(val) =>
setStateFilter(
Object.keys(State)[Object.values(State).indexOf(val)],
)
}
/>
<Divider />
<Dropdown
width="100%"
maxHeight="160px"
options={Object.values(availableLevel)}
selected={availableLevel[levelFilter]}
onSelected={(val) =>
setLevelFilter(Object.values(availableLevel).indexOf(val))
}
/>
</Section>
<Section
title="Tickets"
scrollable
fill
height="450px"
width="300px"
>
<Tabs vertical>
<Tabs.Tab onClick={() => act('new_ticket')}>
New Ticket
<Icon name="plus" ml={0.5} />
</Tabs.Tab>
<Divider />
{filtered_tickets.map((ticket) => (
<Tabs.Tab
key={ticket.id}
selected={ticket.id === selected_ticket?.id}
onClick={() => act('pick_ticket', { ticket_id: ticket.id })}
>
<Box inline>
<Box>
<Button color={LevelColor[ticket.level]}>
{availableLevel[ticket.level]}
</Button>
{ticket.name}
</Box>
<Box fontSize={0.9} textColor={StateColor[ticket.state]}>
State: {State[ticket.state]} | Assignee:
{ticket.handler}
</Box>
</Box>
</Tabs.Tab>
))}
</Tabs>
</Section>
</Stack.Item>
<Stack.Item grow>
{(selected_ticket && (
<Stack fill vertical>
<Stack.Item>
<Section
title={'Ticket #' + selected_ticket.id}
buttons={
<Box nowrap>
<Button
icon="arrow-up"
onClick={() => act('undock_ticket')}
>
Undock
</Button>
<Button
icon="pen"
onClick={() => act('retitle_ticket')}
>
Rename Ticket
</Button>
<Button onClick={() => act('legacy')}>Legacy UI</Button>
<Button color={LevelColor[selected_ticket.level]}>
{availableLevel[selected_ticket.level]}
</Button>
</Box>
}
>
<LabeledList>
<LabeledList.Item label="Ticket ID">
#{selected_ticket.id}:
<div
dangerouslySetInnerHTML={{
__html: selected_ticket.name,
}}
/>
</LabeledList.Item>
<LabeledList.Item label="Type">
{availableLevel[selected_ticket.level]}
</LabeledList.Item>
<LabeledList.Item label="State">
{State[selected_ticket.state]}
</LabeledList.Item>
<LabeledList.Item label="Assignee">
{selected_ticket.handler}
</LabeledList.Item>
{State[selected_ticket.state] === State.open ? (
<LabeledList.Item label="Opened At">
{selected_ticket.opened_at_date +
' (' +
toFixed(
round((selected_ticket.opened_at / 600) * 10, 0) /
10,
1,
) +
' minutes ago.)'}
</LabeledList.Item>
) : (
<LabeledList.Item label="Closed At">
{selected_ticket.closed_at_date +
' (' +
toFixed(
round((selected_ticket.closed_at / 600) * 10, 0) /
10,
1,
) +
' minutes ago.)'}
<Button onClick={() => act('reopen_ticket')}>
Reopen
</Button>
</LabeledList.Item>
)}
<LabeledList.Item label="Actions">
<div
dangerouslySetInnerHTML={{
__html: selected_ticket.actions,
}}
/>
</LabeledList.Item>
<LabeledList.Item label="Log" />
</LabeledList>
</Section>
<Divider />
</Stack.Item>
<Stack.Item grow>
<Section fill ref={messagesEndRef} scrollable>
<Stack fill direction="column">
<Stack.Item grow>
{Object.keys(selected_ticket.log)
.slice(0)
.map((L, i) => (
<div
key={i}
dangerouslySetInnerHTML={{
__html: selected_ticket.log[L],
}}
/>
))}
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item>
<Section fill>
<Stack fill>
<Stack.Item grow>
<Input
autoFocus
autoSelect
fluid
placeholder="Enter a message..."
value={ticketChat}
onChange={(value: string) => setTicketChat(value)}
onKeyDown={(e) => {
if (KEY.Enter === e.key) {
act('send_msg', { msg: ticketChat });
setTicketChat('');
}
}}
/>
</Stack.Item>
<Stack.Item>
<Button
onClick={() => {
act('send_msg', { msg: ticketChat });
setTicketChat('');
}}
>
Send
</Button>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
)) || (
<Section
title="No ticket selected"
buttons={
<Box nowrap>
<Button
disabled
icon="arrow-up"
onClick={() => act('undock_ticket')}
>
Undock
</Button>
<Button
disabled
icon="pen"
onClick={() => act('retitle_ticket')}
>
Rename Ticket
</Button>
<Button onClick={() => act('legacy')}>Legacy UI</Button>
</Box>
}
>
Please select a ticket on the left to view its details.
</Section>
)}
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
+2 -1
View File
@@ -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:
+5 -4
View File
@@ -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"