diff --git a/code/__defines/color.dm b/code/__defines/color.dm index ecf01528aa..c3db40eefb 100644 --- a/code/__defines/color.dm +++ b/code/__defines/color.dm @@ -166,3 +166,9 @@ #define COLOR_ASTEROID_ROCK "#735555" #define COLOR_GOLD "#ffcc33" + +// Discord requires colors to be in decimal instead of hexadecimal. +#define COLOR_WEBHOOK_DEFAULT 0x8bbbd5 // "#8bbbd5" +#define COLOR_WEBHOOK_GOOD 0x2ECC71 // "#2ECC71" +#define COLOR_WEBHOOK_POOR 0xE67E22 // "#E67E22" +#define COLOR_WEBHOOK_BAD 0xE74C3C // "#E74C3C" \ No newline at end of file diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 5aca7e0a99..91c3d508b5 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -448,4 +448,17 @@ GLOBAL_LIST_INIT(all_volume_channels, list( #define LOADOUT_WHITELIST_OFF 0 #define LOADOUT_WHITELIST_LAX 1 -#define LOADOUT_WHITELIST_STRICT 2 \ No newline at end of file +#define LOADOUT_WHITELIST_STRICT 2 + + +#ifndef WINDOWS_HTTP_POST_DLL_LOCATION +#define WINDOWS_HTTP_POST_DLL_LOCATION "lib/byhttp.dll" +#endif + +#ifndef UNIX_HTTP_POST_DLL_LOCATION +#define UNIX_HTTP_POST_DLL_LOCATION "lib/libbyhttp.so" +#endif + +#ifndef HTTP_POST_DLL_LOCATION +#define HTTP_POST_DLL_LOCATION (world.system_type == MS_WINDOWS ? WINDOWS_HTTP_POST_DLL_LOCATION : UNIX_HTTP_POST_DLL_LOCATION) +#endif \ No newline at end of file diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index 831155c1d7..a91df482b9 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -52,6 +52,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G // Subsystem init_order, from highest priority to lowest priority // Subsystems shutdown in the reverse of the order they initialize in // The numbers just define the ordering, they are meaningless otherwise. +#define INIT_ORDER_WEBHOOKS 50 #define INIT_ORDER_DBCORE 41 //CHOMPEdit #define INIT_ORDER_SQLITE 40 #define INIT_ORDER_CHEMISTRY 35 diff --git a/code/__defines/webhooks.dm b/code/__defines/webhooks.dm new file mode 100644 index 0000000000..13fcfa90e4 --- /dev/null +++ b/code/__defines/webhooks.dm @@ -0,0 +1,10 @@ +// Please don't forget to update the webhooks page on the GitHub Wiki page with your new webhook ID. +#define WEBHOOK_ROUNDEND "webhook_roundend" +#define WEBHOOK_ROUNDPREP "webhook_roundprep" +#define WEBHOOK_ROUNDSTART "webhook_roundstart" + +#define WEBHOOK_SUBMAP_LOADED "webhook_submap_loaded" +#define WEBHOOK_CUSTOM_EVENT "webhook_custom_event" +#define WEBHOOK_ELEVATOR_FALL "webhook_elevator_fall" +#define WEBHOOK_AHELP_SENT "webhook_ahelp_sent" +#define WEBHOOK_FAX_SENT "webhook_fax_sent" diff --git a/code/_helpers/_lists.dm b/code/_helpers/_lists.dm index abe05b5141..a87307ac80 100644 --- a/code/_helpers/_lists.dm +++ b/code/_helpers/_lists.dm @@ -855,4 +855,15 @@ proc/dd_sortedTextList(list/incoming) result += pick(shifts) return result - \ No newline at end of file + +var/global/list/json_cache = list() +/proc/cached_json_decode(var/json_to_decode) + if(!json_to_decode || !length(json_to_decode)) + return list() + try + if(isnull(global.json_cache[json_to_decode])) + global.json_cache[json_to_decode] = json_decode(json_to_decode) + . = global.json_cache[json_to_decode] + catch(var/exception/e) + log_error("Exception during JSON decoding ([json_to_decode]): [e]") + return list() \ No newline at end of file diff --git a/code/_helpers/text.dm b/code/_helpers/text.dm index 0bed9abe52..91dd5901e1 100644 --- a/code/_helpers/text.dm +++ b/code/_helpers/text.dm @@ -509,3 +509,23 @@ proc/TextPreview(var/string,var/len=40) var/charcount = count - length_char(text) var/list/chars_to_add[max(charcount + 1, 0)] return text + jointext(chars_to_add, char) + +//Readds quotes and apostrophes to HTML-encoded strings +/proc/readd_quotes(var/t) + var/list/repl_chars = list(""" = "\"","'" = "'") + for(var/char in repl_chars) + var/index = findtext(t, char) + while(index) + t = copytext(t, 1, index) + repl_chars[char] + copytext(t, index+5) + index = findtext(t, char) + return t + +// Rips out paper HTML but tries to keep it semi-readable. +/proc/paper_html_to_plaintext(paper_text) + paper_text = replacetext(paper_text, "
", "\n")
+ paper_text = replacetext(paper_text, "
", "\n")
+ paper_text = strip_html_properly(paper_text) // Get rid of everything else entirely.
+ return paper_text
diff --git a/code/_helpers/text_vr.dm b/code/_helpers/text_vr.dm
deleted file mode 100644
index a16b6c0cfc..0000000000
--- a/code/_helpers/text_vr.dm
+++ /dev/null
@@ -1,9 +0,0 @@
-//Readds quotes and apostrophes to HTML-encoded strings
-/proc/readd_quotes(var/t)
- var/list/repl_chars = list(""" = "\"","'" = "'")
- for(var/char in repl_chars)
- var/index = findtext(t, char)
- while(index)
- t = copytext(t, 1, index) + repl_chars[char] + copytext(t, index+5)
- index = findtext(t, char)
- return t
diff --git a/code/_helpers/type2type.dm b/code/_helpers/type2type.dm
index 52dada1cf9..b5659351be 100644
--- a/code/_helpers/type2type.dm
+++ b/code/_helpers/type2type.dm
@@ -397,3 +397,17 @@
return /datum
return text2path(copytext(string_type, 1, last_slash))
+
+//checks if a file exists and contains text
+//returns text as a string if these conditions are met
+/proc/safe_file2text(filename, error_on_invalid_return = TRUE)
+ try
+ if(fexists(filename))
+ . = file2text(filename)
+ if(!. && error_on_invalid_return)
+ error("File empty ([filename])")
+ else if(error_on_invalid_return)
+ error("File not found ([filename])")
+ catch(var/exception/E)
+ if(error_on_invalid_return)
+ error("Exception when loading file as string: [E]")
\ No newline at end of file
diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm
index 801f47120c..0406067b56 100644
--- a/code/controllers/configuration.dm
+++ b/code/controllers/configuration.dm
@@ -292,6 +292,8 @@ var/list/gamemode_cache = list()
var/static/vgs_access_identifier = null // VOREStation Edit - VGS
var/static/vgs_server_port = null // VOREStation Edit - VGS
+
+ var/disable_webhook_embeds = FALSE
/datum/configuration/New()
var/list/L = typesof(/datum/game_mode) - /datum/game_mode
@@ -1017,6 +1019,9 @@ var/list/gamemode_cache = list()
if("use_loyalty_implants")
config.use_loyalty_implants = 1
+
+ if("loadout_whitelist")
+ config.loadout_whitelist = text2num(value)
else
log_misc("Unknown setting in configuration: '[name]'")
diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm
index 813b106138..90e4254d2b 100644
--- a/code/controllers/subsystems/ticker.dm
+++ b/code/controllers/subsystems/ticker.dm
@@ -49,6 +49,13 @@ var/global/datum/controller/subsystem/ticker/ticker
/datum/controller/subsystem/ticker/Initialize()
pregame_timeleft = config.pregame_time
send2mainirc("Server lobby is loaded and open at byond://[config.serverurl ? config.serverurl : (config.server ? config.server : "[world.address]:[world.port]")]")
+ SSwebhooks.send(
+ WEBHOOK_ROUNDPREP,
+ list(
+ "map" = station_name(),
+ "url" = get_world_url()
+ )
+ )
GLOB.autospeaker = new (null, null, null, 1) //Set up Global Announcer
return ..()
diff --git a/code/controllers/subsystems/webhooks.dm b/code/controllers/subsystems/webhooks.dm
new file mode 100644
index 0000000000..25252d3050
--- /dev/null
+++ b/code/controllers/subsystems/webhooks.dm
@@ -0,0 +1,94 @@
+SUBSYSTEM_DEF(webhooks)
+ name = "Webhooks"
+ init_order = INIT_ORDER_WEBHOOKS
+ flags = SS_NO_FIRE
+ var/list/webhook_decls = list()
+
+/datum/controller/subsystem/webhooks/Initialize()
+ load_webhooks()
+ . = ..()
+
+/datum/controller/subsystem/webhooks/proc/load_webhooks()
+
+ if(!fexists(HTTP_POST_DLL_LOCATION))
+ to_world_log("Unable to locate HTTP POST lib at [HTTP_POST_DLL_LOCATION], webhooks will not function on this run.")
+ return
+
+ var/list/all_webhooks_by_id = list()
+ var/list/all_webhooks = decls_repository.get_decls_of_subtype(/decl/webhook)
+ for(var/wid in all_webhooks)
+ var/decl/webhook/webhook = all_webhooks[wid]
+ if(webhook.id)
+ all_webhooks_by_id[webhook.id] = webhook
+
+ webhook_decls.Cut()
+ var/webhook_config = safe_file2text("config/webhooks.json")
+ if(webhook_config)
+ for(var/webhook_data in cached_json_decode(webhook_config))
+ var/wid = webhook_data["id"]
+ var/wurl = webhook_data["url"]
+ var/list/wmention = webhook_data["mentions"]
+ if(wmention && !islist(wmention))
+ wmention = list(wmention)
+ to_world_log("Setting up webhook [wid].")
+ if(wid && wurl && all_webhooks_by_id[wid])
+ var/decl/webhook/webhook = all_webhooks_by_id[wid]
+ webhook.urls = islist(wurl) ? wurl : list(wurl)
+ for(var/url in webhook.urls)
+ if(!webhook.urls[url])
+ webhook.urls[url] = list()
+ else if(!islist(webhook.urls[url]))
+ webhook.urls[url] = list(webhook.urls[url])
+ if(wmention)
+ webhook.mentions = wmention?.Copy()
+ webhook_decls[wid] = webhook
+ to_world_log("Webhook [wid] ready.")
+ else
+ to_world_log("Failed to set up webhook [wid].")
+
+/datum/controller/subsystem/webhooks/proc/send(var/wid, var/wdata)
+ var/decl/webhook/webhook = webhook_decls[wid]
+ if(webhook)
+ if(webhook.send(wdata))
+ to_world_log("Sent webhook [webhook.id].")
+ log_debug("Webhook sent: [webhook.id].")
+ else
+ to_world_log("Failed to send webhook [webhook.id].")
+ log_debug("Webhook failed to send: [webhook.id].")
+
+/client/proc/reload_webhooks()
+ set name = "Reload Webhooks"
+ set category = "Debug"
+
+ if(!holder)
+ return
+
+ if(!SSwebhooks.subsystem_initialized)
+ to_chat(usr, SPAN_WARNING("Let the webhook subsystem initialize before trying to reload it."))
+ return
+
+ to_world_log("[usr.key] has reloaded webhooks.")
+ log_and_message_admins("has reloaded webhooks.")
+ SSwebhooks.load_webhooks()
+
+/client/proc/ping_webhook()
+ set name = "Ping Webhook"
+ set category = "Debug"
+
+ if(!holder)
+ return
+
+ if(!length(SSwebhooks.webhook_decls))
+ to_chat(usr, SPAN_WARNING("Webhook list is empty; either webhooks are disabled, webhooks aren't configured, or the subsystem hasn't initialized."))
+ return
+
+ var/choice = input(usr, "Select a webhook to ping.", "Ping Webhook") as null|anything in SSwebhooks.webhook_decls
+ if(choice && SSwebhooks.webhook_decls[choice])
+ var/decl/webhook/webhook = SSwebhooks.webhook_decls[choice]
+ log_and_message_admins("has pinged webhook [choice].", usr)
+ to_world_log("[usr.key] has pinged webhook [choice].")
+ webhook.send()
+
+/hook/roundstart/proc/run_webhook()
+ SSwebhooks.send(WEBHOOK_ROUNDSTART, list("url" = get_world_url()))
+ return 1
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index 5a6d420837..17de38aae3 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -396,6 +396,15 @@ var/global/list/additional_antag_types = list()
feedback_set("escaped_on_cryopod",escaped_on_cryopod)
send2mainirc("A round of [src.name] has ended - [surviving_total] survivors, [ghosts] ghosts.")
+ SSwebhooks.send(
+ WEBHOOK_ROUNDEND,
+ list(
+ "survivors" = surviving_total,
+ "escaped" = escaped_total,
+ "ghosts" = ghosts,
+ "clients" = clients
+ )
+ )
return 0
diff --git a/code/game/world.dm b/code/game/world.dm
index e8ac68cee4..190f284603 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -712,3 +712,39 @@ proc/establish_old_db_connection()
SStimer?.reset_buckets()
#undef FAILED_DB_CONNECTION_CUTOFF
+
+/proc/get_world_url()
+ . = "byond://"
+ if(config.serverurl)
+ . += config.serverurl
+ else if(config.server)
+ . += config.server
+ else
+ . += "[world.address]:[world.port]"
+
+var/global/game_id = null
+
+/hook/startup/proc/generate_gameid()
+ if(game_id != null)
+ return
+ game_id = ""
+
+ var/list/c = list(
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"
+ )
+ var/l = c.len
+
+ var/t = world.timeofday
+ for(var/_ = 1 to 4)
+ game_id = "[c[(t % l) + 1]][game_id]"
+ t = round(t / l)
+ game_id = "-[game_id]"
+ t = round(world.realtime / (10 * 60 * 60 * 24))
+ for(var/_ = 1 to 3)
+ game_id = "[c[(t % l) + 1]][game_id]"
+ t = round(t / l)
+ return 1
\ No newline at end of file
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index 8cbc5f8246..4f0475bc74 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -1632,6 +1632,19 @@ datum/admins/var/obj/item/weapon/paper/admin/faxreply // var to hold fax replies
for(var/client/C in GLOB.admins)
if((R_ADMIN | R_MOD | R_EVENT) & C.holder.rights)
to_chat(C, "")
+
+ var/plaintext_title = P.sender ? "replied to [key_name(P.sender)]'s fax" : "sent a fax message to [destination.department]"
+ var/fax_text = paper_html_to_plaintext(P.info)
+ log_game(plaintext_title)
+ log_game(fax_text)
+
+ SSwebhooks.send(
+ WEBHOOK_FAX_SENT,
+ list(
+ "name" = "[key_name(owner)] [plaintext_title].",
+ "body" = fax_text
+ )
+ )
else
to_chat(src.owner, "Message reply failed.")
diff --git a/code/modules/admin/admin_verb_lists.dm b/code/modules/admin/admin_verb_lists.dm
index 50bf3a4d8c..b6ea1889f4 100644
--- a/code/modules/admin/admin_verb_lists.dm
+++ b/code/modules/admin/admin_verb_lists.dm
@@ -239,7 +239,9 @@ var/list/admin_verbs_debug = list(
/client/proc/admin_give_modifier,
/client/proc/simple_DPS,
/datum/admins/proc/view_feedback,
- /client/proc/debug_global_variables
+ /client/proc/debug_global_variables,
+ /client/proc/ping_webhook,
+ /client/proc/reload_webhooks
)
var/list/admin_verbs_paranoid_debug = list(
diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm
index 9d89d54fc5..b511637915 100644
--- a/code/modules/admin/verbs/adminhelp.dm
+++ b/code/modules/admin/verbs/adminhelp.dm
@@ -213,6 +213,17 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
else
ahelp_discord_message("ADMINHELP: FROM: [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") //CHOMPEdit
//YW EDIT END
+
+ // Also send it to discord since that's the hip cool thing now.
+ SSwebhooks.send(
+ WEBHOOK_AHELP_SENT,
+ list(
+ "name" = "Ticket ([id]) (Game ID: [game_id]) ticket opened.",
+ "body" = "[key_name(initiator)] has opened a ticket. \n[msg]",
+ "color" = COLOR_WEBHOOK_POOR
+ )
+ )
+
GLOB.ahelp_tickets.active_tickets += src
/datum/admin_help/Destroy()
@@ -305,6 +316,14 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
feedback_inc("ahelp_reopen")
TicketPanel() //can only be done from here, so refresh it
+ SSwebhooks.send(
+ WEBHOOK_AHELP_SENT,
+ list(
+ "name" = "Ticket ([id]) (Game ID: [game_id]) reopened.",
+ "body" = "Reopened by [key_name(usr)]."
+ )
+ )
+
//private
/datum/admin_help/proc/RemoveActive()
if(state != AHELP_ACTIVE)
@@ -330,6 +349,14 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
var/msg = "Ticket [TicketHref("#[id]")] closed by [key_name_admin(usr)]."
message_admins(msg)
log_admin(msg)
+ SSwebhooks.send(
+ WEBHOOK_AHELP_SENT,
+ list(
+ "name" = "Ticket ([id]) (Game ID: [game_id]) closed.",
+ "body" = "Closed by [key_name(usr)].",
+ "color" = COLOR_WEBHOOK_BAD
+ )
+ )
//Mark open ticket as resolved/legitimate, returns ahelp verb
/datum/admin_help/proc/Resolve(silent = FALSE)
@@ -347,6 +374,14 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name_admin(usr)]"
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
+ )
+ )
//Close and return ahelp verb, use if ticket is incoherent
/datum/admin_help/proc/Reject(key_name = key_name_admin(usr))
@@ -367,6 +402,14 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
log_admin(msg)
AddInteraction("Rejected by [key_name_admin(usr)].")
Close(silent = TRUE)
+ SSwebhooks.send(
+ WEBHOOK_AHELP_SENT,
+ list(
+ "name" = "Ticket ([id]) (Game ID: [game_id]) rejected.",
+ "body" = "Rejected by [key_name(usr)].",
+ "color" = COLOR_WEBHOOK_BAD
+ )
+ )
//Resolve ticket with IC Issue message
/datum/admin_help/proc/ICIssue(key_name = key_name_admin(usr))
@@ -386,6 +429,14 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
log_admin(msg)
AddInteraction("Marked as IC issue by [key_name_admin(usr)]")
Resolve(silent = TRUE)
+ SSwebhooks.send(
+ WEBHOOK_AHELP_SENT,
+ list(
+ "name" = "Ticket ([id]) (Game ID: [game_id]) marked as IC issue.",
+ "body" = "Marked as IC Issue by [key_name(usr)].",
+ "color" = COLOR_WEBHOOK_BAD
+ )
+ )
//Resolve ticket with IC Issue message
/datum/admin_help/proc/HandleIssue()
@@ -397,11 +448,18 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
if(initiator)
to_chat(initiator, msg)
- feedback_inc("ahelp_icissue")
+ feedback_inc("ahelp_handling")
msg = "Ticket [TicketHref("#[id]")] being handled by [key_name(usr,FALSE,FALSE)]"
message_admins(msg)
log_admin(msg)
AddInteraction("[key_name_admin(usr)] is now handling this ticket.")
+ 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()
diff --git a/code/modules/admin/verbs/custom_event.dm b/code/modules/admin/verbs/custom_event.dm
index 5d9cfd3269..6b92e9932e 100644
--- a/code/modules/admin/verbs/custom_event.dm
+++ b/code/modules/admin/verbs/custom_event.dm
@@ -24,6 +24,13 @@
to_world("[custom_event_msg]")
to_world("
")
+ SSwebhooks.send(
+ WEBHOOK_CUSTOM_EVENT,
+ list(
+ "text" = custom_event_msg,
+ )
+ )
+
// normal verb for players to view info
/client/verb/cmd_view_custom_event()
set category = "OOC"
diff --git a/code/modules/paperwork/faxmachine.dm b/code/modules/paperwork/faxmachine.dm
index e30bbf9fbf..fe6b79ae16 100644
--- a/code/modules/paperwork/faxmachine.dm
+++ b/code/modules/paperwork/faxmachine.dm
@@ -227,6 +227,21 @@ var/list/adminfaxes = list() //cache for faxes that have been sent to admins
sleep(50)
visible_message("[src] beeps, \"Message transmitted successfully.\"")
+// Turns objects into just text.
+/obj/machinery/photocopier/faxmachine/proc/make_summary(obj/item/sent)
+ if(istype(sent, /obj/item/weapon/paper))
+ var/obj/item/weapon/paper/P = sent
+ return P.info
+ if(istype(sent, /obj/item/weapon/paper_bundle))
+ . = ""
+ var/obj/item/weapon/paper_bundle/B = sent
+ for(var/i in 1 to B.pages.len)
+ var/obj/item/weapon/paper/P = B.pages[i]
+ if(istype(P)) // Photos can show up here too.
+ if(.) // Space out different pages.
+ . += "
"
+ . += "PAGE [i] - [P.name]
"
+ . += P.info
/obj/machinery/photocopier/faxmachine/proc/message_admins(var/mob/sender, var/faxname, var/obj/item/sent, var/reply_type, font_colour="#006100")
var/msg = "[faxname]: [get_options_bar(sender, 2,1,1)]"
@@ -242,3 +257,24 @@ var/list/adminfaxes = list() //cache for faxes that have been sent to admins
var/faxid = export_fax(sent)
message_chat_admins(sender, faxname, sent, faxid, font_colour)
// VoreStation Edit End
+
+ // Webhooks don't parse the HTML on the paper, so we gotta strip them out so it's still readable.
+ var/summary = make_summary(sent)
+ summary = paper_html_to_plaintext(summary)
+
+ log_game("Fax to [lowertext(faxname)] was sent by [key_name(sender)].")
+ log_game(summary)
+
+ var/webhook_length_limit = 1900 // The actual limit is a little higher.
+ if(length(summary) > webhook_length_limit)
+ summary = copytext(summary, 1, webhook_length_limit + 1)
+ summary += "\n\[Truncated\]"
+
+ SSwebhooks.send(
+ WEBHOOK_FAX_SENT,
+ list(
+ "name" = "[faxname] '[sent.name]' sent from [key_name(sender)]",
+ "body" = summary
+ )
+ )
+
\ No newline at end of file
diff --git a/code/modules/webhooks/_webhook.dm b/code/modules/webhooks/_webhook.dm
new file mode 100644
index 0000000000..e7cfb646cd
--- /dev/null
+++ b/code/modules/webhooks/_webhook.dm
@@ -0,0 +1,72 @@
+/decl/webhook
+ var/id
+ var/list/urls
+ var/list/mentions
+
+/decl/webhook/proc/get_message(var/list/data)
+ . = list()
+
+/decl/webhook/proc/http_post(var/target_url, var/payload)
+ if (!target_url)
+ return -1
+
+ var/result = call(HTTP_POST_DLL_LOCATION, "send_post_request")(target_url, payload, json_encode(list("Content-Type" = "application/json")))
+
+ result = cached_json_decode(result)
+ if (result["error_code"])
+ log_debug("byhttp error: [result["error"]] ([result["error_code"]])")
+ return result["error_code"]
+
+ return list(
+ "status_code" = result["status_code"],
+ "body" = result["body"]
+ )
+
+/decl/webhook/proc/send(var/list/data)
+ var/list/message = get_message(data)
+ if(!length(message))
+ return FALSE
+
+ if(config.disable_webhook_embeds)
+ var/list/embed_content
+ for(var/list/embed in message["embeds"])
+ if(embed["title"])
+ LAZYADD(embed_content, "**[embed["title"]]**")
+ if(embed["description"])
+ LAZYADD(embed_content, embed["description"])
+ if(length(embed_content))
+ if(message["content"])
+ message["content"] = "[message["content"]]\n[jointext(embed_content, "\n")]"
+ else
+ message["content"] = jointext(embed_content, "\n")
+ message -= "embeds"
+
+ . = TRUE
+ for(var/target_url in urls)
+
+ var/url_message = message.Copy()
+ var/list/url_mentions = get_mentions(target_url)
+ if(islist(url_mentions) && length(url_mentions))
+ if(url_message["content"])
+ url_message["content"] = "[jointext(url_mentions, ", ")]: [url_message["content"]]"
+ else
+ url_message["content"] = "[jointext(url_mentions, ", ")]"
+
+ var/list/httpresponse = http_post(target_url, json_encode(url_message))
+ if(!islist(httpresponse))
+ . = FALSE
+ continue
+ switch(httpresponse["status_code"])
+ if (200 to 299)
+ continue
+ if (400 to 599)
+ log_debug("Webhooks: HTTP error code while sending to '[target_url]': [httpresponse["status_code"]]. Data: [httpresponse["body"]].")
+ else
+ log_debug("Webhooks: unknown HTTP code while sending to '[target_url]': [httpresponse["status_code"]]. Data: [httpresponse["body"]].")
+ . = FALSE
+
+/decl/webhook/proc/get_mentions(var/mentioning_url)
+ . = mentions?.Copy()
+ var/url_mentions = LAZYACCESS(urls, mentioning_url)
+ if(length(url_mentions))
+ LAZYDISTINCTADD(., url_mentions)
diff --git a/code/modules/webhooks/webhook_ahelp2discord.dm b/code/modules/webhooks/webhook_ahelp2discord.dm
new file mode 100644
index 0000000000..34241709b7
--- /dev/null
+++ b/code/modules/webhooks/webhook_ahelp2discord.dm
@@ -0,0 +1,13 @@
+/decl/webhook/ahelp_sent
+ id = WEBHOOK_AHELP_SENT
+
+/decl/webhook/ahelp_sent/get_message(var/list/data)
+ .= ..()
+ .["embeds"] = list(list(
+ "title" = "[data["name"]]",
+ "description" = data["body"],
+ "color" = data["color"] || COLOR_WEBHOOK_DEFAULT
+ ))
+
+/decl/webhook/ahelp_sent/get_mentions()
+ . = !length(GLOB.admins) && ..() // VOREStation Edit - GLOB admins
\ No newline at end of file
diff --git a/code/modules/webhooks/webhook_custom_event.dm b/code/modules/webhooks/webhook_custom_event.dm
new file mode 100644
index 0000000000..5f636db256
--- /dev/null
+++ b/code/modules/webhooks/webhook_custom_event.dm
@@ -0,0 +1,11 @@
+/decl/webhook/custom_event
+ id = WEBHOOK_CUSTOM_EVENT
+
+// Data expects a "text" field containing the new custom event text.
+/decl/webhook/custom_event/get_message(var/list/data)
+ . = ..()
+ .["embeds"] = list(list(
+ "title" = "A custom event is beginning.",
+ "description" = (data && data["text"]) || "undefined",
+ "color" = COLOR_WEBHOOK_DEFAULT
+ ))
diff --git a/code/modules/webhooks/webhook_fax2discord.dm b/code/modules/webhooks/webhook_fax2discord.dm
new file mode 100644
index 0000000000..336a01d150
--- /dev/null
+++ b/code/modules/webhooks/webhook_fax2discord.dm
@@ -0,0 +1,10 @@
+/decl/webhook/fax_sent
+ id = WEBHOOK_FAX_SENT
+
+/decl/webhook/fax_sent/get_message(var/list/data)
+ .= ..()
+ .["embeds"] = list(list(
+ "title" = "[data["name"]]",
+ "description" = data["body"],
+ "color" = COLOR_WEBHOOK_DEFAULT
+ ))
\ No newline at end of file
diff --git a/code/modules/webhooks/webhook_roundend.dm b/code/modules/webhooks/webhook_roundend.dm
new file mode 100644
index 0000000000..9c806e5a1d
--- /dev/null
+++ b/code/modules/webhooks/webhook_roundend.dm
@@ -0,0 +1,26 @@
+/decl/webhook/roundend
+ id = WEBHOOK_ROUNDEND
+
+// Data expects three numerical fields: "survivors", "escaped", "ghosts", "clients"
+/decl/webhook/roundend/get_message(var/list/data)
+ . = ..()
+ var/desc = "A round of **[SSticker.mode ? SSticker.mode.name : "Unknown"]** ([game_id]) has ended.\n\n"
+ if(data)
+ var/s_escaped = "Escaped"
+ if(!emergency_shuttle.evac)
+ s_escaped = "Transferred"
+ if(data["survivors"] > 0)
+ desc += "Survivors: **[data["survivors"]]**\n"
+ desc += "[s_escaped]: **[data["escaped"]]**\n"
+ else
+ desc += "There were **no survivors**.\n\n"
+ desc += "Ghosts: **[data["ghosts"]]**\n"
+ desc += "Players: **[data["clients"]]**\n"
+ desc += "Round duration: **[roundduration2text()]**"
+
+ .["embeds"] = list(list(
+ // "title" = global.end_credits_title,
+ "title" = "Round Has Ended",
+ "description" = desc,
+ "color" = COLOR_WEBHOOK_DEFAULT
+ ))
diff --git a/code/modules/webhooks/webhook_roundprep.dm b/code/modules/webhooks/webhook_roundprep.dm
new file mode 100644
index 0000000000..b10580452f
--- /dev/null
+++ b/code/modules/webhooks/webhook_roundprep.dm
@@ -0,0 +1,17 @@
+/decl/webhook/roundprep
+ id = WEBHOOK_ROUNDPREP
+
+// Data expects "url" and field pointing to the current hosted server and port to connect on.
+/decl/webhook/roundprep/get_message(var/list/data)
+ . = ..()
+ var/desc = "The server has been started!\n"
+ if(data && data["map"])
+ desc += "Map: **[data["map"]]**\n"
+ if(data && data["url"])
+ desc += "Address: <[data["url"]]>"
+
+ .["embeds"] = list(list(
+ "title" = "New round is being set up.",
+ "description" = desc,
+ "color" = COLOR_WEBHOOK_DEFAULT
+ ))
diff --git a/code/modules/webhooks/webhook_roundstart.dm b/code/modules/webhooks/webhook_roundstart.dm
new file mode 100644
index 0000000000..f4afaa2bc4
--- /dev/null
+++ b/code/modules/webhooks/webhook_roundstart.dm
@@ -0,0 +1,16 @@
+/decl/webhook/roundstart
+ id = WEBHOOK_ROUNDSTART
+
+// Data expects a "url" field pointing to the current hosted server and port to connect on.
+/decl/webhook/roundstart/get_message(var/list/data)
+ . = ..()
+ var/desc = "Gamemode: **[SSticker.mode.name]**\n"
+ desc += "Players: **[global.player_list.len]**"
+ if(data && data["url"])
+ desc += "\nAddress: <[data["url"]]>"
+
+ .["embeds"] = list(list(
+ "title" = "Round has started.",
+ "description" = desc,
+ "color" = COLOR_WEBHOOK_DEFAULT
+ ))
diff --git a/config/example/webhooks.json b/config/example/webhooks.json
new file mode 100644
index 0000000000..fc8ea883fb
--- /dev/null
+++ b/config/example/webhooks.json
@@ -0,0 +1,12 @@
+[
+ {
+ "id" : "webhook_roundend",
+ "url" : {
+ "someurl0" : [],
+ "someurl1" : [],
+ "someurl2" : "somemention0",
+ "someurl3" : [ "somemention1", "somemention2" ]
+ },
+ "mentions" : [ "somemention3", "somemention4" ]
+ }
+]
diff --git a/vorestation.dme b/vorestation.dme
index bdffd3288d..3bbf628b31 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -104,6 +104,7 @@
#include "code\__defines\unit_tests.dm"
#include "code\__defines\vote.dm"
#include "code\__defines\vv.dm"
+#include "code\__defines\webhooks.dm"
#include "code\__defines\wires.dm"
#include "code\__defines\xenoarcheaology.dm"
#include "code\__defines\ZAS.dm"
@@ -145,7 +146,6 @@
#include "code\_helpers\storage.dm"
#include "code\_helpers\string_lists.dm"
#include "code\_helpers\text.dm"
-#include "code\_helpers\text_vr.dm"
#include "code\_helpers\time.dm"
#include "code\_helpers\turfs.dm"
#include "code\_helpers\type2type.dm"
@@ -309,6 +309,7 @@
#include "code\controllers\subsystems\timer.dm"
#include "code\controllers\subsystems\transcore_vr.dm"
#include "code\controllers\subsystems\vote.dm"
+#include "code\controllers\subsystems\webhooks.dm"
#include "code\controllers\subsystems\xenoarch.dm"
#include "code\controllers\subsystems\processing\bellies_vr.dm"
#include "code\controllers\subsystems\processing\fastprocess.dm"
@@ -4162,6 +4163,13 @@
#include "code\modules\vore\resizing\sizegun_vr.dm"
#include "code\modules\vore\smoleworld\smoleworld_vr.dm"
#include "code\modules\vore\weight\fitness_machines_vr.dm"
+#include "code\modules\webhooks\_webhook.dm"
+#include "code\modules\webhooks\webhook_ahelp2discord.dm"
+#include "code\modules\webhooks\webhook_custom_event.dm"
+#include "code\modules\webhooks\webhook_fax2discord.dm"
+#include "code\modules\webhooks\webhook_roundend.dm"
+#include "code\modules\webhooks\webhook_roundprep.dm"
+#include "code\modules\webhooks\webhook_roundstart.dm"
#include "code\modules\xenoarcheaology\anomaly_container.dm"
#include "code\modules\xenoarcheaology\boulder.dm"
#include "code\modules\xenoarcheaology\effect.dm"