diff --git a/code/__defines/color.dm b/code/__defines/color.dm index 36745128134..3cb9715586b 100644 --- a/code/__defines/color.dm +++ b/code/__defines/color.dm @@ -165,3 +165,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 bb0387ab4ee..143eaceb37c 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 d24f0bfdd60..bb6592c5760 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_SQLITE 40 #define INIT_ORDER_CHEMISTRY 35 #define INIT_ORDER_SKYBOX 30 diff --git a/code/__defines/webhooks.dm b/code/__defines/webhooks.dm new file mode 100644 index 00000000000..13fcfa90e47 --- /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 abe05b5141f..a87307ac804 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 2d59fa6b6a8..4a26745a6a9 100644 --- a/code/_helpers/text.dm +++ b/code/_helpers/text.dm @@ -507,3 +507,26 @@ 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) +<<<<<<< HEAD +======= + +//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
+>>>>>>> 4deffc7... Ports Nebula's Discord Webhook Integration (#8071)
diff --git a/code/_helpers/type2type.dm b/code/_helpers/type2type.dm
index 52dada1cf96..b5659351be3 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 1f82939221a..48432ff3f6b 100644
--- a/code/controllers/configuration.dm
+++ b/code/controllers/configuration.dm
@@ -287,6 +287,7 @@ var/list/gamemode_cache = list()
var/static/enable_night_shifts = FALSE
// How strictly the loadout enforces object species whitelists
+<<<<<<< HEAD
var/loadout_whitelist = LOADOUT_WHITELIST_LAX
var/static/vgs_access_identifier = null // VOREStation Edit - VGS
@@ -1126,3 +1127,839 @@ var/list/gamemode_cache = list()
config.python_path = "/usr/bin/env python2"
else //probably windows, if not this should work anyway
config.python_path = "python"
+=======
+ var/loadout_whitelist = LOADOUT_WHITELIST_LAX
+
+ var/disable_webhook_embeds = FALSE
+
+/datum/configuration/New()
+ var/list/L = typesof(/datum/game_mode) - /datum/game_mode
+ for (var/T in L)
+ // I wish I didn't have to instance the game modes in order to look up
+ // their information, but it is the only way (at least that I know of).
+ var/datum/game_mode/M = new T()
+ if (M.config_tag)
+ gamemode_cache[M.config_tag] = M // So we don't instantiate them repeatedly.
+ if(!(M.config_tag in modes)) // ensure each mode is added only once
+ log_misc("Adding game mode [M.name] ([M.config_tag]) to configuration.")
+ modes += M.config_tag
+ mode_names[M.config_tag] = M.name
+ probabilities[M.config_tag] = M.probability
+ player_requirements[M.config_tag] = M.required_players
+ player_requirements_secret[M.config_tag] = M.required_players_secret
+ if (M.votable)
+ src.votable_modes += M.config_tag
+ src.votable_modes += "secret"
+
+/datum/configuration/proc/load(filename, type = "config") //the type can also be game_options, in which case it uses a different switch. not making it separate to not copypaste code - Urist
+ var/list/Lines = file2list(filename)
+
+ for(var/t in Lines)
+ if(!t) continue
+
+ t = trim(t)
+ if (length(t) == 0)
+ continue
+ else if (copytext(t, 1, 2) == "#")
+ continue
+
+ var/pos = findtext(t, " ")
+ var/name = null
+ var/value = null
+
+ if (pos)
+ name = lowertext(copytext(t, 1, pos))
+ value = copytext(t, pos + 1)
+ else
+ name = lowertext(t)
+
+ if (!name)
+ continue
+
+ if(type == "config")
+ switch (name)
+ if ("resource_urls")
+ config.resource_urls = splittext(value, " ")
+
+ if ("admin_legacy_system")
+ config.admin_legacy_system = 1
+
+ if ("ban_legacy_system")
+ config.ban_legacy_system = 1
+
+ if ("use_age_restriction_for_jobs")
+ config.use_age_restriction_for_jobs = 1
+
+ if ("use_age_restriction_for_antags")
+ config.use_age_restriction_for_antags = 1
+
+ if ("jobs_have_minimal_access")
+ config.jobs_have_minimal_access = 1
+
+ if ("use_recursive_explosions")
+ use_recursive_explosions = 1
+
+ if ("multi_z_explosion_scalar")
+ multi_z_explosion_scalar = text2num(value)
+
+ if ("log_ooc")
+ config.log_ooc = 1
+
+ if ("log_access")
+ config.log_access = 1
+
+ if ("sql_enabled")
+ config.sql_enabled = 1
+
+ if ("log_say")
+ config.log_say = 1
+
+ if ("debug_paranoid")
+ config.debugparanoid = 1
+
+ if ("log_admin")
+ config.log_admin = 1
+
+ if ("log_debug")
+ config.log_debug = text2num(value)
+
+ if ("log_game")
+ config.log_game = 1
+
+ if ("log_vote")
+ config.log_vote = 1
+
+ if ("log_whisper")
+ config.log_whisper = 1
+
+ if ("log_attack")
+ config.log_attack = 1
+
+ if ("log_emote")
+ config.log_emote = 1
+
+ if ("log_adminchat")
+ config.log_adminchat = 1
+
+ if ("log_adminwarn")
+ config.log_adminwarn = 1
+
+ if ("log_pda")
+ config.log_pda = 1
+
+ if ("log_world_output")
+ config.log_world_output = 1
+
+ if ("log_hrefs")
+ config.log_hrefs = 1
+
+ if ("log_runtime")
+ config.log_runtime = 1
+
+ if ("log_graffiti")
+ config.log_graffiti = 1
+
+ if ("generate_map")
+ config.generate_map = 1
+
+ if ("no_click_cooldown")
+ config.no_click_cooldown = 1
+
+ if("allow_admin_ooccolor")
+ config.allow_admin_ooccolor = 1
+
+ if ("allow_vote_restart")
+ config.allow_vote_restart = 1
+
+ if ("allow_vote_mode")
+ config.allow_vote_mode = 1
+
+ if ("allow_admin_jump")
+ config.allow_admin_jump = 1
+
+ if("allow_admin_rev")
+ config.allow_admin_rev = 1
+
+ if ("allow_admin_spawning")
+ config.allow_admin_spawning = 1
+
+ if ("allow_byond_links")
+ allow_byond_links = 1
+
+ if ("allow_discord_links")
+ allow_discord_links = 1
+
+ if ("allow_url_links")
+ allow_url_links = 1
+
+ if ("no_dead_vote")
+ config.vote_no_dead = 1
+
+ if ("default_no_vote")
+ config.vote_no_default = 1
+
+ if ("pregame_time")
+ config.pregame_time = text2num(value)
+
+ if ("vote_delay")
+ config.vote_delay = text2num(value)
+
+ if ("vote_period")
+ config.vote_period = text2num(value)
+
+ if ("vote_autotransfer_initial")
+ config.vote_autotransfer_initial = text2num(value)
+
+ if ("vote_autotransfer_interval")
+ config.vote_autotransfer_interval = text2num(value)
+
+ if ("vote_autogamemode_timeleft")
+ config.vote_autogamemode_timeleft = text2num(value)
+
+ if("ert_admin_only")
+ config.ert_admin_call_only = 1
+
+ if ("allow_ai")
+ config.allow_ai = 1
+
+ if ("allow_ai_shells")
+ config.allow_ai_shells = TRUE
+
+ if("give_free_ai_shell")
+ config.give_free_ai_shell = TRUE
+
+// if ("authentication")
+// config.enable_authentication = 1
+
+ if ("norespawn")
+ config.respawn = 0
+
+ if ("respawn_time")
+ var/raw_minutes = text2num(value)
+ config.respawn_time = raw_minutes MINUTES
+
+ if ("respawn_message")
+ config.respawn_message = "[value]"
+
+ if ("servername")
+ config.server_name = value
+
+ if ("serversuffix")
+ config.server_suffix = 1
+
+ if ("nudge_script_path")
+ config.nudge_script_path = value
+
+ if ("hostedby")
+ config.hostedby = value
+
+ if ("serverurl")
+ config.serverurl = value
+
+ if ("server")
+ config.server = value
+
+ if ("banappeals")
+ config.banappeals = value
+
+ if ("wikiurl")
+ config.wikiurl = value
+
+ if ("wikisearchurl")
+ config.wikisearchurl = value
+
+ if ("forumurl")
+ config.forumurl = value
+
+ if ("rulesurl")
+ config.rulesurl = value
+
+ if ("mapurl")
+ config.mapurl = value
+
+ if ("githuburl")
+ config.githuburl = value
+ if ("guest_jobban")
+ config.guest_jobban = 1
+
+ if ("guest_ban")
+ config.guests_allowed = 0
+
+ if ("disable_ooc")
+ config.ooc_allowed = 0
+ config.looc_allowed = 0
+
+ if ("disable_entry")
+ config.enter_allowed = 0
+
+ if ("disable_dead_ooc")
+ config.dooc_allowed = 0
+
+ if ("disable_dsay")
+ config.dsay_allowed = 0
+
+ if ("disable_respawn")
+ config.abandon_allowed = 0
+
+ if ("usewhitelist")
+ config.usewhitelist = 1
+
+ if ("feature_object_spell_system")
+ config.feature_object_spell_system = 1
+
+ if ("allow_metadata")
+ config.allow_Metadata = 1
+
+ if ("traitor_scaling")
+ config.traitor_scaling = 1
+
+ if ("aliens_allowed")
+ config.aliens_allowed = 1
+
+ if ("ninjas_allowed")
+ config.ninjas_allowed = 1
+
+ if ("objectives_disabled")
+ config.objectives_disabled = 1
+
+ if("protect_roles_from_antagonist")
+ config.protect_roles_from_antagonist = 1
+
+ if("persistence_disabled")
+ config.persistence_disabled = TRUE // Previously this forcibly set persistence enabled in the saves.
+
+ if("persistence_ignore_mapload")
+ config.persistence_ignore_mapload = TRUE
+
+ if ("probability")
+ var/prob_pos = findtext(value, " ")
+ var/prob_name = null
+ var/prob_value = null
+
+ if (prob_pos)
+ prob_name = lowertext(copytext(value, 1, prob_pos))
+ prob_value = copytext(value, prob_pos + 1)
+ if (prob_name in config.modes)
+ config.probabilities[prob_name] = text2num(prob_value)
+ else
+ log_misc("Unknown game mode probability configuration definition: [prob_name].")
+ else
+ log_misc("Incorrect probability configuration definition: [prob_name] [prob_value].")
+
+ if ("required_players", "required_players_secret")
+ var/req_pos = findtext(value, " ")
+ var/req_name = null
+ var/req_value = null
+ var/is_secret_override = findtext(name, "required_players_secret") // Being extra sure we're not picking up an override for Secret by accident.
+
+ if(req_pos)
+ req_name = lowertext(copytext(value, 1, req_pos))
+ req_value = copytext(value, req_pos + 1)
+ if(req_name in config.modes)
+ if(is_secret_override)
+ config.player_requirements_secret[req_name] = text2num(req_value)
+ else
+ config.player_requirements[req_name] = text2num(req_value)
+ else
+ log_misc("Unknown game mode player requirement configuration definition: [req_name].")
+ else
+ log_misc("Incorrect player requirement configuration definition: [req_name] [req_value].")
+
+ if("allow_random_events")
+ config.allow_random_events = 1
+
+ if("enable_game_master")
+ config.enable_game_master = 1
+
+ if("kick_inactive")
+ config.kick_inactive = text2num(value)
+
+ if("show_mods")
+ config.show_mods = 1
+
+ if("show_devs")
+ config.show_devs = 1
+
+ if("show_event_managers")
+ config.show_event_managers = 1
+
+ if("mods_can_tempban")
+ config.mods_can_tempban = 1
+
+ if("mods_can_job_tempban")
+ config.mods_can_job_tempban = 1
+
+ if("mod_tempban_max")
+ config.mod_tempban_max = text2num(value)
+
+ if("mod_job_tempban_max")
+ config.mod_job_tempban_max = text2num(value)
+
+ if("load_jobs_from_txt")
+ load_jobs_from_txt = 1
+
+ if("alert_red_upto")
+ config.alert_desc_red_upto = value
+
+ if("alert_red_downto")
+ config.alert_desc_red_downto = value
+
+ if("alert_blue_downto")
+ config.alert_desc_blue_downto = value
+
+ if("alert_blue_upto")
+ config.alert_desc_blue_upto = value
+
+ if("alert_green")
+ config.alert_desc_green = value
+
+ if("alert_delta")
+ config.alert_desc_delta = value
+
+ if("forbid_singulo_possession")
+ forbid_singulo_possession = 1
+
+ if("popup_admin_pm")
+ config.popup_admin_pm = 1
+
+ if("allow_holidays")
+ Holiday = 1
+
+ if("use_irc_bot")
+ use_irc_bot = 1
+
+ if("use_node_bot")
+ use_node_bot = 1
+
+ if("irc_bot_port")
+ config.irc_bot_port = value
+
+ if("irc_bot_export")
+ irc_bot_export = 1
+
+ if("ticklag")
+ var/ticklag = text2num(value)
+ if(ticklag > 0)
+ fps = 10 / ticklag
+
+ if("tick_limit_mc_init")
+ tick_limit_mc_init = text2num(value)
+
+ if("allow_antag_hud")
+ config.antag_hud_allowed = 1
+ if("antag_hud_restricted")
+ config.antag_hud_restricted = 1
+
+ if("socket_talk")
+ socket_talk = text2num(value)
+
+ if("tickcomp")
+ Tickcomp = 1
+
+ if("humans_need_surnames")
+ humans_need_surnames = 1
+
+ if("tor_ban")
+ ToRban = 1
+
+ if("automute_on")
+ automute_on = 1
+
+ if("usealienwhitelist")
+ usealienwhitelist = 1
+
+ if("alien_player_ratio")
+ limitalienplayers = 1
+ alien_to_human_ratio = text2num(value)
+
+ if("assistant_maint")
+ config.assistant_maint = 1
+
+ if("gateway_delay")
+ config.gateway_delay = text2num(value)
+
+ if("continuous_rounds")
+ config.continous_rounds = 1
+
+ if("ghost_interaction")
+ config.ghost_interaction = 1
+
+ if("disable_player_mice")
+ config.disable_player_mice = 1
+
+ if("uneducated_mice")
+ config.uneducated_mice = 1
+
+ if("comms_password")
+ config.comms_password = value
+
+ if("irc_bot_host")
+ config.irc_bot_host = value
+
+ if("main_irc")
+ config.main_irc = value
+
+ if("admin_irc")
+ config.admin_irc = value
+
+ if("python_path")
+ if(value)
+ config.python_path = value
+
+ if("use_lib_nudge")
+ config.use_lib_nudge = 1
+
+ if("allow_cult_ghostwriter")
+ config.cult_ghostwriter = 1
+
+ if("req_cult_ghostwriter")
+ config.cult_ghostwriter_req_cultists = text2num(value)
+
+ if("character_slots")
+ config.character_slots = text2num(value)
+
+ if("loadout_slots")
+ config.loadout_slots = text2num(value)
+
+ if("allow_drone_spawn")
+ config.allow_drone_spawn = text2num(value)
+
+ if("drone_build_time")
+ config.drone_build_time = text2num(value)
+
+ if("max_maint_drones")
+ config.max_maint_drones = text2num(value)
+
+ if("use_overmap")
+ config.use_overmap = 1
+
+ if("engine_map")
+ config.engine_map = splittext(value, ",")
+/*
+ if("station_levels")
+ using_map.station_levels = text2numlist(value, ";")
+
+ if("admin_levels")
+ using_map.admin_levels = text2numlist(value, ";")
+
+ if("contact_levels")
+ using_map.contact_levels = text2numlist(value, ";")
+
+ if("player_levels")
+ using_map.player_levels = text2numlist(value, ";")
+*/
+ if("expected_round_length")
+ config.expected_round_length = MinutesToTicks(text2num(value))
+
+ if("disable_welder_vision")
+ config.welder_vision = 0
+
+ if("allow_extra_antags")
+ config.allow_extra_antags = 1
+
+ if("event_custom_start_mundane")
+ var/values = text2numlist(value, ";")
+ config.event_first_run[EVENT_LEVEL_MUNDANE] = list("lower" = MinutesToTicks(values[1]), "upper" = MinutesToTicks(values[2]))
+
+ if("event_custom_start_moderate")
+ var/values = text2numlist(value, ";")
+ config.event_first_run[EVENT_LEVEL_MODERATE] = list("lower" = MinutesToTicks(values[1]), "upper" = MinutesToTicks(values[2]))
+
+ if("event_custom_start_major")
+ var/values = text2numlist(value, ";")
+ config.event_first_run[EVENT_LEVEL_MAJOR] = list("lower" = MinutesToTicks(values[1]), "upper" = MinutesToTicks(values[2]))
+
+ if("event_delay_lower")
+ var/values = text2numlist(value, ";")
+ config.event_delay_lower[EVENT_LEVEL_MUNDANE] = MinutesToTicks(values[1])
+ config.event_delay_lower[EVENT_LEVEL_MODERATE] = MinutesToTicks(values[2])
+ config.event_delay_lower[EVENT_LEVEL_MAJOR] = MinutesToTicks(values[3])
+
+ if("event_delay_upper")
+ var/values = text2numlist(value, ";")
+ config.event_delay_upper[EVENT_LEVEL_MUNDANE] = MinutesToTicks(values[1])
+ config.event_delay_upper[EVENT_LEVEL_MODERATE] = MinutesToTicks(values[2])
+ config.event_delay_upper[EVENT_LEVEL_MAJOR] = MinutesToTicks(values[3])
+
+ if("starlight")
+ value = text2num(value)
+ config.starlight = value >= 0 ? value : 0
+
+ if("ert_species")
+ config.ert_species = splittext(value, ";")
+ if(!config.ert_species.len)
+ config.ert_species += SPECIES_HUMAN
+
+ if("law_zero")
+ law_zero = value
+
+ if("aggressive_changelog")
+ config.aggressive_changelog = 1
+
+ if("default_language_prefixes")
+ var/list/values = splittext(value, " ")
+ if(values.len > 0)
+ language_prefixes = values
+
+ if("radiation_lower_limit")
+ radiation_lower_limit = text2num(value)
+
+ if("radiation_resistance_calc_divide")
+ radiation_resistance_calc_mode = RAD_RESIST_CALC_DIV
+
+ if("radiation_resistance_calc_subtract")
+ radiation_resistance_calc_mode = RAD_RESIST_CALC_SUB
+
+ if("radiation_resistance_multiplier")
+ radiation_resistance_multiplier = text2num(value)
+
+ if("radiation_material_resistance_divisor")
+ radiation_material_resistance_divisor = text2num(value)
+
+ if("radiation_decay_rate")
+ radiation_decay_rate = text2num(value)
+
+ if ("panic_bunker")
+ config.panic_bunker = 1
+
+ if ("paranoia_logging")
+ config.paranoia_logging = 1
+
+ if("ip_reputation")
+ config.ip_reputation = 1
+
+ if("ipr_email")
+ config.ipr_email = value
+
+ if("ipr_block_bad_ips")
+ config.ipr_block_bad_ips = 1
+
+ if("ipr_bad_score")
+ config.ipr_bad_score = text2num(value)
+
+ if("ipr_allow_existing")
+ config.ipr_allow_existing = 1
+
+ if("ipr_minimum_age")
+ config.ipr_minimum_age = text2num(value)
+
+ if("random_submap_orientation")
+ config.random_submap_orientation = 1
+
+ if("autostart_solars")
+ config.autostart_solars = TRUE
+
+ if("sqlite_enabled")
+ config.sqlite_enabled = TRUE
+
+ if("sqlite_feedback")
+ config.sqlite_feedback = TRUE
+
+ if("sqlite_feedback_topics")
+ config.sqlite_feedback_topics = splittext(value, ";")
+ if(!config.sqlite_feedback_topics.len)
+ config.sqlite_feedback_topics += "General"
+
+ if("sqlite_feedback_privacy")
+ config.sqlite_feedback_privacy = TRUE
+
+ if("sqlite_feedback_cooldown")
+ config.sqlite_feedback_cooldown = text2num(value)
+
+ if("defib_timer")
+ config.defib_timer = text2num(value)
+
+ if("defib_braindamage_timer")
+ config.defib_braindamage_timer = text2num(value)
+
+ if("disable_cid_warn_popup")
+ config.disable_cid_warn_popup = TRUE
+
+ if("enable_night_shifts")
+ config.enable_night_shifts = TRUE
+
+ else
+ log_misc("Unknown setting in configuration: '[name]'")
+
+ else if(type == "game_options")
+ if(!value)
+ log_misc("Unknown value for setting [name] in [filename].")
+ value = text2num(value)
+
+ switch(name)
+ if("health_threshold_crit")
+ config.health_threshold_crit = value
+ if("health_threshold_softcrit")
+ config.health_threshold_softcrit = value
+ if("health_threshold_dead")
+ config.health_threshold_dead = value
+ if("show_human_death_message")
+ config.show_human_death_message = 1
+ if("revival_pod_plants")
+ config.revival_pod_plants = value
+ if("revival_cloning")
+ config.revival_cloning = value
+ if("revival_brain_life")
+ config.revival_brain_life = value
+ if("organ_health_multiplier")
+ config.organ_health_multiplier = value / 100
+ if("organ_regeneration_multiplier")
+ config.organ_regeneration_multiplier = value / 100
+ if("organ_damage_spillover_multiplier")
+ config.organ_damage_spillover_multiplier = value / 100
+ if("organs_can_decay")
+ config.organs_decay = 1
+ if("default_brain_health")
+ config.default_brain_health = text2num(value)
+ if(!config.default_brain_health || config.default_brain_health < 1)
+ config.default_brain_health = initial(config.default_brain_health)
+ if("bones_can_break")
+ config.bones_can_break = value
+ if("limbs_can_break")
+ config.limbs_can_break = value
+ if("allow_headgibs")
+ config.allow_headgibs = TRUE
+
+ if("run_speed")
+ config.run_speed = value
+ if("walk_speed")
+ config.walk_speed = value
+
+ if("human_delay")
+ config.human_delay = value
+ if("robot_delay")
+ config.robot_delay = value
+ if("monkey_delay")
+ config.monkey_delay = value
+ if("alien_delay")
+ config.alien_delay = value
+ if("slime_delay")
+ config.slime_delay = value
+ if("animal_delay")
+ config.animal_delay = value
+
+ if("footstep_volume")
+ config.footstep_volume = text2num(value)
+
+ 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]'")
+
+/datum/configuration/proc/loadsql(filename) // -- TLE
+ var/list/Lines = file2list(filename)
+ for(var/t in Lines)
+ if(!t) continue
+
+ t = trim(t)
+ if (length(t) == 0)
+ continue
+ else if (copytext(t, 1, 2) == "#")
+ continue
+
+ var/pos = findtext(t, " ")
+ var/name = null
+ var/value = null
+
+ if (pos)
+ name = lowertext(copytext(t, 1, pos))
+ value = copytext(t, pos + 1)
+ else
+ name = lowertext(t)
+
+ if (!name)
+ continue
+
+ switch (name)
+ if ("address")
+ sqladdress = value
+ if ("port")
+ sqlport = value
+ if ("database")
+ sqldb = value
+ if ("login")
+ sqllogin = value
+ if ("password")
+ sqlpass = value
+ if ("feedback_database")
+ sqlfdbkdb = value
+ if ("feedback_login")
+ sqlfdbklogin = value
+ if ("feedback_password")
+ sqlfdbkpass = value
+ if ("enable_stat_tracking")
+ sqllogging = 1
+ else
+ log_misc("Unknown setting in configuration: '[name]'")
+
+/datum/configuration/proc/loadforumsql(filename) // -- TLE
+ var/list/Lines = file2list(filename)
+ for(var/t in Lines)
+ if(!t) continue
+
+ t = trim(t)
+ if (length(t) == 0)
+ continue
+ else if (copytext(t, 1, 2) == "#")
+ continue
+
+ var/pos = findtext(t, " ")
+ var/name = null
+ var/value = null
+
+ if (pos)
+ name = lowertext(copytext(t, 1, pos))
+ value = copytext(t, pos + 1)
+ else
+ name = lowertext(t)
+
+ if (!name)
+ continue
+
+ switch (name)
+ if ("address")
+ forumsqladdress = value
+ if ("port")
+ forumsqlport = value
+ if ("database")
+ forumsqldb = value
+ if ("login")
+ forumsqllogin = value
+ if ("password")
+ forumsqlpass = value
+ if ("activatedgroup")
+ forum_activated_group = value
+ if ("authenticatedgroup")
+ forum_authenticated_group = value
+ else
+ log_misc("Unknown setting in configuration: '[name]'")
+
+/datum/configuration/proc/pick_mode(mode_name)
+ // I wish I didn't have to instance the game modes in order to look up
+ // their information, but it is the only way (at least that I know of).
+ for (var/game_mode in gamemode_cache)
+ var/datum/game_mode/M = gamemode_cache[game_mode]
+ if (M.config_tag && M.config_tag == mode_name)
+ return M
+ return gamemode_cache["extended"]
+
+/datum/configuration/proc/get_runnable_modes()
+ var/list/runnable_modes = list()
+ for(var/game_mode in gamemode_cache)
+ var/datum/game_mode/M = gamemode_cache[game_mode]
+ if(M && M.can_start() && !isnull(config.probabilities[M.config_tag]) && config.probabilities[M.config_tag] > 0)
+ runnable_modes |= M
+ return runnable_modes
+
+/datum/configuration/proc/post_load()
+ //apply a default value to config.python_path, if needed
+ if (!config.python_path)
+ if(world.system_type == UNIX)
+ config.python_path = "/usr/bin/env python2"
+ else //probably windows, if not this should work anyway
+ config.python_path = "python"
+>>>>>>> 4deffc7... Ports Nebula's Discord Webhook Integration (#8071)
diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm
index 47f35a27a82..7d7b123298d 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 00000000000..25252d30504
--- /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 a1df0327933..98a41a2be42 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -368,6 +368,15 @@ var/global/list/additional_antag_types = list()
feedback_set("escaped_on_pod_5",escaped_on_pod_5)
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 a51b7185de0..acd9845e5c7 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -677,3 +677,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 b70af8354d5..9f50f3be1d7 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -1630,6 +1630,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 7f4db66b3a0..bee29ed14f8 100644
--- a/code/modules/admin/admin_verb_lists.dm
+++ b/code/modules/admin/admin_verb_lists.dm
@@ -235,7 +235,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 e294514b914..98436a2cec4 100644
--- a/code/modules/admin/verbs/adminhelp.dm
+++ b/code/modules/admin/verbs/adminhelp.dm
@@ -183,7 +183,22 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
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)
+<<<<<<< HEAD
to_chat(C, "No active admins are online, your adminhelp was sent to the admin discord.") //VOREStation Edit
+=======
+ to_chat(C, "No active admins are online, your adminhelp was sent to the admin irc.")
+
+ // 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
+ )
+ )
+
+>>>>>>> 4deffc7... Ports Nebula's Discord Webhook Integration (#8071)
GLOB.ahelp_tickets.active_tickets += src
/datum/admin_help/Destroy()
@@ -273,6 +288,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)
@@ -298,6 +321,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)
@@ -315,6 +346,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))
@@ -335,6 +374,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))
@@ -354,6 +401,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()
@@ -365,11 +420,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 5d9cfd32691..6b92e9932e2 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 9f4a183b31e..226aedcd5a9 100644
--- a/code/modules/paperwork/faxmachine.dm
+++ b/code/modules/paperwork/faxmachine.dm
@@ -222,6 +222,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)]"
@@ -232,8 +247,31 @@ var/list/adminfaxes = list() //cache for faxes that have been sent to admins
if(check_rights((R_ADMIN|R_MOD|R_EVENT),0,C))
to_chat(C,msg)
C << 'sound/machines/printer.ogg'
+<<<<<<< HEAD
// VoreStation Edit Start
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
+ )
+ )
+>>>>>>> 4deffc7... Ports Nebula's Discord Webhook Integration (#8071)
diff --git a/code/modules/webhooks/_webhook.dm b/code/modules/webhooks/_webhook.dm
new file mode 100644
index 00000000000..e7cfb646cd7
--- /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 00000000000..3ae72d3bfe5
--- /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(global.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 00000000000..5f636db2569
--- /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 00000000000..336a01d1500
--- /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 00000000000..9c806e5a1dc
--- /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 00000000000..b10580452f2
--- /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 00000000000..f4afaa2bc4a
--- /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 00000000000..fc8ea883fbc
--- /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 aac8ba93e52..b8ff6a2e893 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -97,6 +97,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"
@@ -290,6 +291,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"
@@ -3819,6 +3821,7 @@
#include "code\modules\virus2\helpers.dm"
#include "code\modules\virus2\isolator.dm"
#include "code\modules\virus2\items_devices.dm"
+<<<<<<< HEAD:vorestation.dme
#include "code\modules\vore\hook-defs_vr.dm"
#include "code\modules\vore\trycatch_vr.dm"
#include "code\modules\vore\appearance\preferences_vr.dm"
@@ -3849,6 +3852,15 @@
#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"
+>>>>>>> 4deffc7... Ports Nebula's Discord Webhook Integration (#8071):polaris.dme
#include "code\modules\xenoarcheaology\anomaly_container.dm"
#include "code\modules\xenoarcheaology\boulder.dm"
#include "code\modules\xenoarcheaology\effect.dm"