diff --git a/code/controllers/subsystems/job.dm b/code/controllers/subsystems/job.dm
index f265f39955a..2aa6d84cb69 100644
--- a/code/controllers/subsystems/job.dm
+++ b/code/controllers/subsystems/job.dm
@@ -131,12 +131,13 @@ SUBSYSTEM_DEF(job)
return department_datums[primary_department]
+/datum/controller/subsystem/job/proc/get_ping_role(var/role)
+ var/datum/job/J = get_job(role)
+ if(J.requestable)
+ return get_primary_department_of_job(J)
+
// Someday it might be good to port code/game/jobs/job_controller.dm to here and clean it up.
-
-
-
-
/datum/controller/subsystem/job/proc/job_debug_message(message)
if(debug_messages)
log_debug("JOB DEBUG: [message]")
\ No newline at end of file
diff --git a/code/game/jobs/job/assistant_vr.dm b/code/game/jobs/job/assistant_vr.dm
index 7bfdadca50e..8be93935052 100644
--- a/code/game/jobs/job/assistant_vr.dm
+++ b/code/game/jobs/job/assistant_vr.dm
@@ -27,6 +27,7 @@
job_description = "An Intern does whatever is requested of them, often doing so in process of learning \
another job. Though they are part of the crew, they have no real authority."
timeoff_factor = 0 // Interns, noh
+ requestable = FALSE
/datum/alt_title/intern_eng
title = "Apprentice Engineer"
@@ -96,6 +97,7 @@
supervisors = "nobody! You don't work here"
job_description = "A Visitor is just there to visit the place. They have no real authority or responsibility."
timeoff_factor = 0
+ requestable = FALSE
alt_titles = list("Guest" = /datum/alt_title/guest, "Traveler" = /datum/alt_title/traveler)
/datum/job/assistant/New()
diff --git a/code/game/jobs/job/job_vr.dm b/code/game/jobs/job/job_vr.dm
index 0f2599b8aad..ddff55e061d 100644
--- a/code/game/jobs/job/job_vr.dm
+++ b/code/game/jobs/job/job_vr.dm
@@ -20,6 +20,8 @@
//Do we forbid ourselves from earning PTO?
var/playtime_only = FALSE
+ var/requestable = TRUE
+
// Check client-specific availability rules.
/datum/job/proc/player_has_enough_pto(client/C)
return timeoff_factor >= 0 || (C && LAZYACCESS(C.department_hours, pto_type) > 0)
diff --git a/code/game/jobs/job/special_vr.dm b/code/game/jobs/job/special_vr.dm
index 6e3f1df8277..295a2ad2784 100644
--- a/code/game/jobs/job/special_vr.dm
+++ b/code/game/jobs/job/special_vr.dm
@@ -64,6 +64,7 @@
job_description = "A Clown is there to entertain the crew and keep high morale using various harmless pranks and ridiculous jokes!"
whitelist_only = 1
latejoin_only = 0
+ requestable = FALSE
outfit_type = /decl/hierarchy/outfit/job/clown
pto_type = PTO_CIVILIAN
alt_titles = list("Jester" = /datum/alt_title/jester, "Fool" = /datum/alt_title/fool)
@@ -95,6 +96,7 @@
alt_titles = list("Poseur" = /datum/alt_title/poseur)
whitelist_only = 1
latejoin_only = 0
+ requestable = FALSE
outfit_type = /decl/hierarchy/outfit/job/mime
pto_type = PTO_CIVILIAN
diff --git a/code/modules/paperwork/faxmachine_vr.dm b/code/modules/paperwork/faxmachine_vr.dm
index 9ac4c97884f..d4462b76cd6 100644
--- a/code/modules/paperwork/faxmachine_vr.dm
+++ b/code/modules/paperwork/faxmachine_vr.dm
@@ -1,3 +1,5 @@
+var/global/last_fax_role_request
+
/obj/machinery/photocopier/faxmachine
req_one_access = list()
@@ -47,6 +49,21 @@
query_string += "&sentname=[url_encode(sent.name)]"
world.Export("[config.chat_webhook_url]?[query_string]")
+/**
+ * Call the chat webhook to transmit a notification of a job request
+ */
+/obj/machinery/photocopier/faxmachine/proc/message_chat_rolerequest(var/font_colour="#006100", var/role_to_ping, var/reason, var/jobname)
+ if(config.chat_webhook_url || 1)
+ spawn(0)
+ var/query_string = "type=rolerequest"
+ query_string += "&key=[url_encode(config.chat_webhook_key)]"
+ query_string += "&ping=[url_encode(role_to_ping)]"
+ query_string += "&color=[url_encode(font_colour)]"
+ query_string += "&reason=[url_encode(reason)]"
+ query_string += "&job=[url_encode(jobname)]"
+ world.Export("[config.chat_webhook_url]?[query_string]")
+ to_world(query_string)
+
//
// Overrides/additions to stock defines go here, as well as hooks. Sort them by
// the object they are overriding. So all /mob/living together, etc.
@@ -55,3 +72,80 @@
var/chat_webhook_url = "" // URL of the webhook for sending announcements/faxes to discord chat.
var/chat_webhook_key = "" // Shared secret for authenticating to the chat webhook
var/fax_export_dir = "data/faxes" // Directory in which to write exported fax HTML files.
+
+
+/obj/machinery/photocopier/faxmachine/verb/request_roles()
+ set name = "Staff Request Form"
+ set category = "Object"
+ set src in oview(1)
+
+ var/mob/living/L = usr
+
+ if(!L || !isturf(L.loc) || !isliving(L))
+ return
+ if(!ishuman(L) && !issilicon(L))
+ return
+ if(L.stat || L.restrained())
+ return
+ if(last_fax_role_request && (world.time - last_fax_role_request < 5 MINUTES))
+ to_chat(L, "The global automated relays are still recalibrating. Try again later or relay your request in written form for processing.")
+ return
+
+ var/confirmation = tgui_alert(L, "Are you sure you want to send automated crew request?", "Confirmation", list("Yes", "No", "Cancel"))
+ if(confirmation != "Yes")
+ return
+
+ var/list/jobs = list()
+ for(var/datum/department/dept as anything in SSjob.get_all_department_datums())
+ if(!dept.assignable || dept.centcom_only)
+ continue
+ jobs |= SSjob.get_job_titles_in_department(dept.name)
+
+ var/role = tgui_input_list(L, "Pick the job to request.", "Job Request", jobs)
+ if(!role)
+ return
+
+ var/reason = ""
+ reason = tgui_input_text(L, "Reason (maximum 100 symbols):","Reason")
+ if(length(reason) > 110)
+ to_chat(L, "The reason you've input is too long. Try again briefer.")
+ return
+
+ var/final_conf = tgui_alert(L, "You are about to request [role]. Are you sure?", "Confirmation", list("Yes", "No", "Cancel"))
+ if(final_conf != "Yes")
+ return
+
+ var/datum/department/ping_dept = SSjob.get_ping_role(role)
+ if(!ping_dept)
+ to_chat(L, "Selected job cannot be requested for \[ERRORDEPTNOTFOUND] reason. Please report this to system administrator.")
+ return
+ var/message_color = "#FFFFFF"
+ var/ping_name = null
+ switch(ping_dept.name)
+ if(DEPARTMENT_COMMAND)
+ ping_name = "Command"
+ if(DEPARTMENT_SECURITY)
+ ping_name = "Security"
+ if(DEPARTMENT_ENGINEERING)
+ ping_name = "Engineering"
+ if(DEPARTMENT_MEDICAL)
+ ping_name = "Medical"
+ if(DEPARTMENT_RESEARCH)
+ ping_name = "Research"
+ if(DEPARTMENT_CARGO)
+ ping_name = "Supply"
+ if(DEPARTMENT_CIVILIAN)
+ ping_name = "Service"
+ if(DEPARTMENT_PLANET)
+ ping_name = "Expedition"
+ if(DEPARTMENT_SYNTHETIC)
+ ping_name = "Silicon"
+ //if(DEPARTMENT_TALON)
+ // ping_name = "Offmap"
+ if(!ping_name)
+ to_chat(L, "Selected job cannot be requested for \[ERRORUNKNOWNDEPT] reason. Please report this to system administrator.")
+ return
+ message_color = ping_dept.color
+
+ message_chat_rolerequest(message_color, ping_name, reason, role)
+ last_fax_role_request = world.time
\ No newline at end of file