Skip to content

Commit

Permalink
SShttp + SSdiscord | ASYNCHRONOUS STUFF IN BYOND! (ParadiseSS13#14762)
Browse files Browse the repository at this point in the history
* SShttp + SSdiscord | ASYNCHRONOUS STUFF IN BYOND!

* Cleanup

* HTTP Callback example

* Fixes rust instability

* More refactors

* This works

* The sanitizer (Now worth £3000)

* New configs + other stuff

* Lets give this a shot

* Farie changes

* Mentor support

* Farie fixes
  • Loading branch information
AffectedArc07 authored Nov 7, 2020
1 parent dbe4e00 commit 56752b0
Show file tree
Hide file tree
Showing 29 changed files with 652 additions and 213 deletions.
2 changes: 1 addition & 1 deletion _build_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ export BYOND_MAJOR=513
# Byond Minor
export BYOND_MINOR=1526
# For the RUSTG library
export RUSTG_VERSION=2.0
export RUSTG_VERSION=2.1-P
9 changes: 9 additions & 0 deletions code/__DEFINES/misc.dm
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,12 @@
#define LINDA_SPAWN_N2O 64
#define LINDA_SPAWN_AGENT_B 128
#define LINDA_SPAWN_AIR 256

/// Send to the primary Discord webhook
#define DISCORD_WEBHOOK_PRIMARY "PRIMARY"

/// Send to the admin Discord webhook
#define DISCORD_WEBHOOK_ADMIN "ADMIN"

/// Send to the mentor Discord webhook
#define DISCORD_WEBHOOK_MENTOR "MENTOR"
9 changes: 7 additions & 2 deletions code/__DEFINES/rust_g.dm
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@
#define rustg_log_write(fname, text) call(RUST_G, "log_write")(fname, text)
/proc/rustg_log_close_all() return call(RUST_G, "log_close_all")()

// HTTP library stuff // - AA TODO: Add in SShttp
// HTTP library stuff //
#define RUSTG_HTTP_METHOD_GET "get"
#define RUSTG_HTTP_METHOD_PUT "put"
#define RUSTG_HTTP_METHOD_DELETE "delete"
#define RUSTG_HTTP_METHOD_PATCH "patch"
#define RUSTG_HTTP_METHOD_HEAD "head"
#define RUSTG_HTTP_METHOD_POST "post"
#define rustg_http_request_blocking(method, url, body, headers) call(RUST_G, "http_request_blocking")(method, url, body, headers)

// Commented out because this thing locks up the entire DD process when you use it
// DO NOT USE FOR THE LOVE OF GOD
// #define rustg_http_request_blocking(method, url, body, headers) call(RUST_G, "http_request_blocking")(method, url, body, headers)
#define rustg_http_request_async(method, url, body, headers) call(RUST_G, "http_request_async")(method, url, body, headers)
#define rustg_http_check_request(req_id) call(RUST_G, "http_check_request")(req_id)
/proc/rustg_create_async_http_client() return call(RUST_G, "start_http_client")()
/proc/rustg_close_async_http_client() return call(RUST_G, "shutdown_http_client")()

// SQL stuff // - AA TODO: Async SQL + SSdbcore
#define rustg_sql_connect_pool(options) call(RUST_G, "sql_connect_pool")(options)
Expand Down
2 changes: 2 additions & 0 deletions code/_globalvars/logging.dm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ GLOBAL_VAR(runtime_summary_log)
GLOBAL_PROTECT(runtime_summary_log)
GLOBAL_VAR(tgui_log)
GLOBAL_PROTECT(tgui_log)
GLOBAL_VAR(http_log)
GLOBAL_PROTECT(http_log)

GLOBAL_LIST_EMPTY(jobMax)
GLOBAL_PROTECT(jobMax)
Expand Down
4 changes: 2 additions & 2 deletions code/_onclick/ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
add_attack_logs(src, src, "[key_name_admin(src)] might be running a modified client! (failed can_see on AI click of [A]([ADMIN_COORDJMP(pixel_turf)]))", ATKLOG_ALL)
var/message = "[key_name(src)] might be running a modified client! (failed can_see on AI click of [A]([COORD(pixel_turf)]))"
log_admin(message)
send2irc_adminless_only("NOCHEAT", "[key_name(src)] might be running a modified client! (failed checkTurfVis on AI click of [A]([COORD(pixel_turf)]))")
SSdiscord.send2discord_simple_noadmins("[key_name(src)] might be running a modified client! (failed checkTurfVis on AI click of [A]([COORD(pixel_turf)]))")


var/turf_visible
Expand All @@ -65,7 +65,7 @@
if(pixel_turf.obscured)
log_admin("[key_name_admin(src)] might be running a modified client! (failed checkTurfVis on AI click of [A]([COORD(pixel_turf)])")
add_attack_logs(src, src, "[key_name_admin(src)] might be running a modified client! (failed checkTurfVis on AI click of [A]([ADMIN_COORDJMP(pixel_turf)]))", ATKLOG_ALL)
send2irc_adminless_only("NOCHEAT", "[key_name(src)] might be running a modified client! (failed checkTurfVis on AI click of [A]([COORD(pixel_turf)]))")
SSdiscord.send2discord_simple_noadmins("[key_name(src)] might be running a modified client! (failed checkTurfVis on AI click of [A]([COORD(pixel_turf)]))")
return

var/list/modifiers = params2list(params)
Expand Down
58 changes: 33 additions & 25 deletions code/controllers/configuration.dm
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,6 @@

var/comms_password = ""

var/use_irc_bot = 0
var/list/irc_bot_host = list()
var/main_irc = ""
var/admin_irc = ""
var/admin_notify_irc = ""
var/cidrandomizer_irc = ""

var/default_laws = 0 //Controls what laws the AI spawns with.

var/list/station_levels = list(1) // Defines which Z-levels the station exists on.
Expand Down Expand Up @@ -253,6 +246,25 @@
/// BYOND account age limit for notifcations of new accounts (Any accounts older than this value will not send notifications on first join)
var/byond_account_age_threshold = 7

/// Are discord webhooks enabled?
var/discord_webhooks_enabled = FALSE

/// Role ID to be pinged for administrative events
var/discord_admin_role_id = null // Intentional null usage

/// Webhook URL for the main public webhook
var/discord_main_webhook_url

/// Webhook URL for the admin webhook
var/discord_admin_webhook_url

/// Webhook URL for the mentor webhook
var/discord_mentor_webhook_url

/// Do we want to forward all adminhelps to the discord or just ahelps when admins are offline.
/// (This does not mean all ahelps are pinged, only ahelps sent when staff are offline get the ping, regardless of this setting)
var/discord_forward_all_ahelps = FALSE

/datum/configuration/New()
for(var/T in subtypesof(/datum/game_mode))
var/datum/game_mode/M = T
Expand Down Expand Up @@ -562,9 +574,6 @@
if("allow_holidays")
config.allow_holidays = 1

if("use_irc_bot")
use_irc_bot = 1

if("ticklag")
Ticklag = text2num(value)

Expand Down Expand Up @@ -605,21 +614,6 @@
if("comms_password")
config.comms_password = value

if("irc_bot_host")
config.irc_bot_host = splittext(value, ";")

if("main_irc")
config.main_irc = value

if("admin_irc")
config.admin_irc = value

if("admin_notify_irc")
config.admin_notify_irc = value

if("cidrandomizer_irc")
config.cidrandomizer_irc = value

if("python_path")
if(value)
GLOB.python_path = value
Expand Down Expand Up @@ -746,6 +740,20 @@
config.enable_gamemode_player_limit = 1
if("byond_account_age_threshold")
config.byond_account_age_threshold = text2num(value)
// Discord stuff
if("enable_discord_webhooks")
discord_webhooks_enabled = TRUE
if("discord_webhooks_admin_role_id")
discord_admin_role_id = "[value]" // This MUST be a string because BYOND doesnt like massive integers
if("discord_webhooks_main_url")
discord_main_webhook_url = value
if("discord_webhooks_admin_url")
discord_admin_webhook_url = value
if("discord_webhooks_mentor_url")
discord_mentor_webhook_url = value
if("discord_forward_all_ahelps")
discord_forward_all_ahelps = TRUE
// End discord stuff
else
log_config("Unknown setting in configuration: '[name]'")

Expand Down
82 changes: 82 additions & 0 deletions code/controllers/subsystem/discord.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
SUBSYSTEM_DEF(discord)
name = "Discord"
flags = SS_NO_FIRE
/// Is the SS enabled
var/enabled = FALSE
/// Last time the administrator ping was dropped. This ensures administrators cannot be mass pinged if a large chunk of ahelps go off at once (IE: tesloose)
var/last_administration_ping = 0

/datum/controller/subsystem/discord/Initialize(start_timeofday)
if(config.discord_webhooks_enabled)
enabled = TRUE
return ..()

// This is designed for ease of simplicity for sending quick messages from parts of the code
/datum/controller/subsystem/discord/proc/send2discord_simple(destination, content)
if(!enabled)
return
var/webhook_url
switch(destination)
if(DISCORD_WEBHOOK_ADMIN)
webhook_url = config.discord_admin_webhook_url
if(DISCORD_WEBHOOK_PRIMARY)
webhook_url = config.discord_main_webhook_url
if(DISCORD_WEBHOOK_MENTOR)
webhook_url = config.discord_mentor_webhook_url

var/datum/discord_webhook_payload/dwp = new()
dwp.webhook_content = content
SShttp.create_async_request(RUSTG_HTTP_METHOD_POST, webhook_url, dwp.serialize2json(), list("content-type" = "application/json"))

// This one is designed to take in a [/datum/discord_webhook_payload] which was prepared beforehand
/datum/controller/subsystem/discord/proc/send2discord_complex(destination, datum/discord_webhook_payload/dwp)
if(!enabled)
return
var/webhook_url
switch(destination)
if(DISCORD_WEBHOOK_ADMIN)
webhook_url = config.discord_admin_webhook_url
if(DISCORD_WEBHOOK_PRIMARY)
webhook_url = config.discord_main_webhook_url
SShttp.create_async_request(RUSTG_HTTP_METHOD_POST, webhook_url, dwp.serialize2json(), list("content-type" = "application/json"))

// This one is for sending messages to the admin channel if no admins are active, complete with a ping to the game admins role
/datum/controller/subsystem/discord/proc/send2discord_simple_noadmins(content, check_send_always = FALSE)
// Setup some stuff
var/alerttext
var/list/admincounter = staff_countup(R_BAN)
var/active_admins = admincounter[1]
var/inactive_admins = admincounter[3]
var/add_ping = TRUE

if(active_admins <= 0)
if(inactive_admins > 0)
alerttext = " | **ALL ADMINS AFK**"
else
alerttext = " | **NO ADMINS ONLINE**"
else
if(check_send_always && config.discord_forward_all_ahelps)
// If we are here, there are admins online. We want to forward everything, but obviously dont want to add a ping, so we do this
add_ping = FALSE
else
// We have active admins, we dont care about the rest of this proc
return

var/message = "[content] [alerttext] [add_ping ? handle_administrator_ping() : ""]"

var/datum/discord_webhook_payload/dwp = new()
dwp.webhook_content = message

SShttp.create_async_request(RUSTG_HTTP_METHOD_POST, config.discord_admin_webhook_url, dwp.serialize2json(), list("content-type" = "application/json"))

// Helper to make administrator ping easier
/datum/controller/subsystem/discord/proc/handle_administrator_ping()
// Check if a role is even set
if(config.discord_admin_role_id)
if(last_administration_ping > world.time)
return "*(Role pinged recently)*"

last_administration_ping = world.time + 60 SECONDS
return "<@&[config.discord_admin_role_id]>"

return "*(Role not configured)*"
120 changes: 120 additions & 0 deletions code/controllers/subsystem/http.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
SUBSYSTEM_DEF(http)
name = "HTTP"
flags = SS_TICKER | SS_BACKGROUND // Measure in ticks, but also only run if we have the spare CPU. We also dont init.
wait = 1
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY // All the time
// Assuming for the worst, since only discord is hooked into this for now, but that may change
offline_implications = "The server is no longer capable of making async HTTP requests. Shuttle call recommended."
/// List of all async HTTP requests in the processing chain
var/list/datum/http_request/active_async_requests
/// Variable to define if logging is enabled or not. Disabled by default since we know the requests the server is making. Enable with VV if you need to debug requests
var/logging_enabled = FALSE

/datum/controller/subsystem/http/Initialize(start_timeofday)
rustg_create_async_http_client() // Open the door
active_async_requests = list()
return ..()

/datum/controller/subsystem/http/fire(resumed)
for(var/r in active_async_requests)
var/datum/http_request/req = r
// Check if we are complete
if(req.is_complete())
// If so, take it out the processing list
active_async_requests -= req
var/datum/http_response/res = req.into_response()

// If the request has a callback, invoke it.Async of course to avoid choking the SS
if(req.cb)
req.cb.InvokeAsync(res)

// And log the result
if(logging_enabled)
var/list/log_data = list()
log_data += "BEGIN ASYNC RESPONSE (ID: [req.id])"
if(res.errored)
log_data += "\t ----- RESPONSE ERRROR -----"
log_data += "\t [res.error]"
else
log_data += "\tResponse status code: [res.status_code]"
log_data += "\tResponse body: [res.body]"
log_data += "\tResponse headers: [json_encode(res.headers)]"
log_data += "END ASYNC RESPONSE (ID: [req.id])"
rustg_log_write(GLOB.http_log, log_data.Join("\n[GLOB.log_end]"))

/**
* Async request creator
*
* Generates an async request, and adds it to the subsystem's processing list
* These should be used as they do not lock the entire DD process up as they execute inside their own thread pool inside RUSTG
*/
/datum/controller/subsystem/http/proc/create_async_request(method, url, body = "", list/headers, datum/callback/proc_callback)
var/datum/http_request/req = new()
req.prepare(method, url, body, headers)
if(proc_callback)
req.cb = proc_callback

// Begin it and add it to the SS active list
req.begin_async()
active_async_requests += req

if(logging_enabled)
// Create a log holder
var/list/log_data = list()
log_data += "BEGIN ASYNC REQUEST (ID: [req.id])"
log_data += "\t[uppertext(req.method)] [req.url]"
log_data += "\tRequest body: [req.body]"
log_data += "\tRequest headers: [req.headers]"
log_data += "END ASYNC REQUEST (ID: [req.id])"

// Write the log data
rustg_log_write(GLOB.http_log, log_data.Join("\n[GLOB.log_end]"))

/**
* Blocking request creator
*
* Generates a blocking request, executes it, logs the info then cleanly returns the response
* Exists as a proof of concept, and should never be used
*/
/datum/controller/subsystem/http/proc/make_blocking_request(method, url, body = "", list/headers)
CRASH("Attempted use of a blocking HTTP request")
/*
var/datum/http_request/req = new()
req.prepare(method, url, body, headers)
req.execute_blocking()
var/datum/http_response/res = req.into_response()
// Now generate a logfile
var/list/log_data = list()
log_data += "NEW BLOCKING REQUEST"
log_data += "\t[uppertext(req.method)] [req.url]"
log_data += "\tRequest body: [req.body]"
log_data += "\tRequest headers: [req.headers]"
if(res.errored)
log_data += "\t ----- RESPONSE ERRROR -----"
log_data += "\t [res.error]"
else
log_data += "\tResponse status code: [res.status_code]"
log_data += "\tResponse body: [res.body]"
log_data += "\tResponse headers: [json_encode(res.headers)]"
log_data += "END BLOCKING REQUEST"
// Write the log data
rustg_log_write(GLOB.http_log, log_data.Join("\n[GLOB.log_end]"))
return res
*/

/*
Example of how to use callbacks properly
/client/verb/testing()
set name = "Testing"
var/datum/callback/cb = CALLBACK(src, /client/.proc/response, usr)
SShttp.create_async_request(RUSTG_HTTP_METHOD_GET, "http://site.domain/page.html", proc_callback=cb)
/client/proc/response(mob/user, datum/http_response/response)
to_chat(user, "<span class='notice'>Code: [response.status_code] | Content: [response.body]")
*/
4 changes: 1 addition & 3 deletions code/controllers/subsystem/ticker.dm
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,7 @@ SUBSYSTEM_DEF(ticker)
//start_events() //handles random events and space dust.
//new random event system is handled from the MC.

var/list/admins_number = staff_countup(R_BAN)
if(admins_number[1] == 0 && admins_number[3] == 0)
send2irc(config.admin_notify_irc, "Round has started with no admins online.")
SSdiscord.send2discord_simple_noadmins("Round has started")
auto_toggle_ooc(0) // Turn it off
round_start_time = world.time

Expand Down
Loading

0 comments on commit 56752b0

Please sign in to comment.