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, "
", "-----") + paper_text = replacetext(paper_text, "
  • ", "- ") // This makes ordered lists turn into unordered but fixing that is too much effort. + paper_text = replacetext(paper_text, "
  • ", "\n") + 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, "FAX LOG:[key_name_admin(src.owner)] has sent a fax message to [destination.department] (VIEW)") + + 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"