From 62f788fbc63455ba4a448fc865b5baa2f2c0c6e2 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 28 Sep 2017 23:19:50 -0400 Subject: [PATCH] Server tools API v3.1 (#31000) * New API for service communication * Safer this way * Gives the game the ability to kill itself * tick_lag * Sanity check * Updates comment * Formalization of the server tools API * Fixes, finishes, and cleanup * Remove unecessary scoping * Compile fixes * Didn't Ctrl+S * Reimplement chat commands * Fixup * Required parameters * Fax * Testing * Fix ON_TOPIC * The more defines the more better * That's bass ackwards * Fix tgs2 * Fuck it, call him pichael * Do this * No, we only use the modern methods now * Remove tgs2 relay support * Remove kebab * Kill kill kill * This is back baby * Missed a GLOB * Remove DownloadPRDetails() * Cache custom commands by name * Adds "notify" chat command * Use the official API * Fix API misuse * Readme licensing memes * Fix API * Moves chat new game announcement to when the API is confirmed compatible * Add TGS3.json * Fix the input options * Removes notify command --- README.md | 2 + TGS3.json | 22 +++ code/__DEFINES/server_tools.dm | 117 ++++++++++++++-- code/__HELPERS/unsorted.dm | 57 -------- .../configuration/entries/config.dm | 3 - code/controllers/subsystem/ticker.dm | 2 + code/datums/helper_datums/getrev.dm | 60 +-------- code/game/world.dm | 44 +----- code/modules/admin/admin.dm | 15 ++- code/modules/admin/chat_commands.dm | 63 +++++++++ code/modules/admin/verbs/adminhelp.dm | 6 +- code/modules/server_tools/nudge.py | 22 --- code/modules/server_tools/server_tools.dm | 93 ------------- code/modules/server_tools/st_commands.dm | 76 +++++++++++ code/modules/server_tools/st_interface.dm | 125 ++++++++++++++++++ config/server_to_tool_bridge_port.txt | 1 - tgstation.dme | 4 +- 17 files changed, 420 insertions(+), 292 deletions(-) create mode 100644 TGS3.json create mode 100644 code/modules/admin/chat_commands.dm delete mode 100644 code/modules/server_tools/nudge.py delete mode 100644 code/modules/server_tools/server_tools.dm create mode 100644 code/modules/server_tools/st_commands.dm create mode 100644 code/modules/server_tools/st_interface.dm delete mode 100644 config/server_to_tool_bridge_port.txt diff --git a/README.md b/README.md index a52beddd1db..c0bd67288f2 100644 --- a/README.md +++ b/README.md @@ -137,8 +137,10 @@ See LICENSE-AGPLv3.txt and LICENSE-GPLv3.txt for more details. tgui clientside is licensed as a subproject under the MIT license. Font Awesome font files, used by tgui, are licensed under the SIL Open Font License v1.1 tgui assets are licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/). +The TGS3 API is licensed as a subproject under the MIT license. See tgui/LICENSE.md for the MIT license. See tgui/assets/fonts/SIL-OFL-1.1-LICENSE.md for the SIL Open Font License. +See the footers of code/\_\_DEFINES/server\_tools.dm, code/modules/server\_tools/st\_commands.dm, and code/modules/server\_tools/st\_inteface.dm for the MIT license. All assets including icons and sound are under a [Creative Commons 3.0 BY-SA license](http://creativecommons.org/licenses/by-sa/3.0/) unless otherwise indicated. diff --git a/TGS3.json b/TGS3.json new file mode 100644 index 00000000000..4fd41ab9f1a --- /dev/null +++ b/TGS3.json @@ -0,0 +1,22 @@ +{ + "documentation": "/tg/station server 3 configuration file", + "changelog": { + "script": "tools/ss13_genchangelog.py", + "arguments": "html/changelog.html html/changelogs", + "pip_dependancies": [ + "PyYaml", + "beautifulsoup4" + ] + }, + "synchronize_paths": [ + "html/changelog.html", + "html/changelogs/*" + ], + "static_directories": [ + "config", + "data" + ], + "dlls": [ + "libmysql.dll" + ] + } \ No newline at end of file diff --git a/code/__DEFINES/server_tools.dm b/code/__DEFINES/server_tools.dm index d2be3e578e4..b86a54959f0 100644 --- a/code/__DEFINES/server_tools.dm +++ b/code/__DEFINES/server_tools.dm @@ -1,34 +1,123 @@ +// /tg/station 13 server tools API v3.1 + +//CONFIGURATION +//use this define if you want to do configuration outside of this file +#ifndef SERVER_TOOLS_EXTERNAL_CONFIGURATION +//Comment this out once you've filled in the below +//#error /tg/station server tools interface unconfigured + +//Required interfaces (fill in with your codebase equivalent): + +//create a global variable named `Name` and set it to `Value` +//These globals must not be modifiable from anywhere outside of the server tools +#define SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(Name, Value) GLOBAL_VAR_INIT(##Name, ##Value); GLOBAL_PROTECT(##Name) +//Read the value in the global variable `Name` +#define SERVER_TOOLS_READ_GLOBAL(Name) GLOB.##Name +//Set the value in the global variable `Name` to `Value` +#define SERVER_TOOLS_WRITE_GLOBAL(Name, Value) GLOB.##Name = ##Value +//display an announcement `message` from the server to all players +#define SERVER_TOOLS_WORLD_ANNOUNCE(message) to_chat(world, "[html_encode(##message)]") +//Write a string `message` to a server log +#define SERVER_TOOLS_LOG(message) log_world("SERVICE: [##message]") +//Notify current in-game administrators of a string `event` +#define SERVER_TOOLS_NOTIFY_ADMINS(event) message_admins(##event) +#endif + +//Required hooks: + +//Put this somewhere in /world/New() that is always run +#define SERVER_TOOLS_ON_NEW ServiceInit() +//Put this somewhere in /world/Topic(T, Addr, Master, Keys) that is always run before T is modified +#define SERVER_TOOLS_ON_TOPIC var/service_topic_return = ServiceCommand(params2list(T)); if(service_topic_return) return service_topic_return +//Put at the beginning of world/Reboot(reason) +#define SERVER_TOOLS_ON_REBOOT ServiceReboot() + +//Optional callable functions: + +//Returns the string version of the API +#define SERVER_TOOLS_API_VERSION ServiceAPIVersion() +//Returns TRUE if the world was launched under the server tools and the API matches, FALSE otherwise +//No function below this succeed if this is FALSE +#define SERVER_TOOLS_PRESENT RunningService() +//Gets the current version of the service running the server +#define SERVER_TOOLS_VERSION ServiceVersion() +//Forces a hard reboot of BYOND by ending the process +//unlike del(world) clients will try to reconnect +//If the service has not requested a shutdown, the world will reboot shortly after +#define SERVER_TOOLS_REBOOT_BYOND world.ServiceEndProcess() +/* + Gets the list of any testmerged github pull requests + + "[PR Number]" => list( + "title" -> PR title + "commit" -> Full hash of commit merged + "author" -> Github username of the author of the PR + ) +*/ +#define SERVER_TOOLS_PR_LIST GetTestMerges() +//Sends a message to connected game chats +#define SERVER_TOOLS_CHAT_BROADCAST(message) world.ChatBroadcast(message) +//Sends a message to connected admin chats +#define SERVER_TOOLS_RELAY_BROADCAST(message) world.AdminBroadcast(message) + +//IMPLEMENTATION + +#define SERVICE_API_VERSION_STRING "3.1.0.0" + #define REBOOT_MODE_NORMAL 0 #define REBOOT_MODE_HARD 1 #define REBOOT_MODE_SHUTDOWN 2 -#define IRC_STATUS_THROTTLE 5 - -#define PR_ANNOUNCEMENTS_PER_ROUND 5 //The number of unique PR announcements allowed per round - //This makes sure that a single person can only spam 3 reopens and 3 closes before being ignored - -//keep these in sync with TGS3 #define SERVICE_WORLD_PARAM "server_service" #define SERVICE_VERSION_PARAM "server_service_version" #define SERVICE_PR_TEST_JSON "prtestjob.json" -#define SERVICE_PR_TEST_JSON_OLD "..\\..\\[SERVICE_PR_TEST_JSON]" +#define SERVICE_INTERFACE_DLL "TGServiceInterface.dll" +#define SERVICE_INTERFACE_FUNCTION "DDEntryPoint" #define SERVICE_CMD_HARD_REBOOT "hard_reboot" #define SERVICE_CMD_GRACEFUL_SHUTDOWN "graceful_shutdown" #define SERVICE_CMD_WORLD_ANNOUNCE "world_announce" -#define SERVICE_CMD_IRC_CHECK "irc_check" -#define SERVICE_CMD_IRC_STATUS "irc_status" -#define SERVICE_CMD_ADMIN_MSG "adminmsg" -#define SERVICE_CMD_NAME_CHECK "namecheck" -#define SERVICE_CMD_ADMIN_WHO "adminwho" +#define SERVICE_CMD_LIST_CUSTOM "list_custom_commands" #define SERVICE_CMD_PARAM_KEY "serviceCommsKey" #define SERVICE_CMD_PARAM_COMMAND "command" -#define SERVICE_CMD_PARAM_MESSAGE "message" -#define SERVICE_CMD_PARAM_TARGET "target" #define SERVICE_CMD_PARAM_SENDER "sender" +#define SERVICE_CMD_PARAM_CUSTOM "custom" +#define SERVICE_CMD_API_COMPATIBLE "api_compat" + +#define SERVICE_JSON_PARAM_HELPTEXT "help_text" +#define SERVICE_JSON_PARAM_ADMINONLY "admin_only" +#define SERVICE_JSON_PARAM_REQUIREDPARAMETERS "required_parameters" #define SERVICE_REQUEST_KILL_PROCESS "killme" #define SERVICE_REQUEST_IRC_BROADCAST "irc" #define SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE "send2irc" #define SERVICE_REQUEST_WORLD_REBOOT "worldreboot" +#define SERVICE_REQUEST_API_VERSION "api_ver" + +/* +The MIT License + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 905babe8488..5e190ab7098 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -1364,63 +1364,6 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) return FALSE return TRUE -//WHATEVER YOU USE THIS FOR MUST BE SANITIZED TO SHIT, IT USES SHELL -//It also sleeps - -//Set this to TRUE before calling -//This prevents RCEs from badmins -//kevinz000 if you touch this I will hunt you down -GLOBAL_VAR_INIT(valid_HTTPSGet, FALSE) -GLOBAL_PROTECT(valid_HTTPSGet) -/proc/HTTPSGet(url) //tgs2 support - if(findtext(url, "\"")) - GLOB.valid_HTTPSGet = FALSE - - if(!GLOB.valid_HTTPSGet) - if(usr) - CRASH("[usr.ckey]([usr]) just attempted an invalid HTTPSGet on: [url]!") - else - CRASH("Invalid HTTPSGet call on: [url]") - GLOB.valid_HTTPSGet = FALSE - - //"This has got to be the ugliest hack I have ever done" - //warning, here be dragons - /* - | @___oo - /\ /\ / (__,,,,| - ) /^\) ^\/ _) - ) /^\/ _) - ) _ / / _) - /\ )/\/ || | )_) - < > |(,,) )__) - || / \)___)\ - | \____( )___) )___ - \______(_______;;; __;;; - */ - var/temp_file = "data/HTTPSGetOutput.txt" - var/command - if(world.system_type == MS_WINDOWS) - command = "powershell -Command \"wget [url] -OutFile [temp_file]\"" - else if(world.system_type == UNIX) - command = "wget -O [temp_file] [url]" - else - CRASH("Invalid world.system_type ([world.system_type])? Yell at Lummox.") - - log_world("HTTPSGet: [url]") - var/result = shell(command) - if(result != 0) - log_world("Download failed: shell exited with code: [result]") - return - - var/f = file(temp_file) - if(!f) - log_world("Download failed: Temp file not found") - return - - . = file2text(f) - f = null - fdel(temp_file) - #define UNTIL(X) while(!(X)) stoplag() /proc/pass() diff --git a/code/controllers/configuration/entries/config.dm b/code/controllers/configuration/entries/config.dm index cabb6dca27f..ed09ca4a9c5 100644 --- a/code/controllers/configuration/entries/config.dm +++ b/code/controllers/configuration/entries/config.dm @@ -227,9 +227,6 @@ CONFIG_DEF(flag/load_jobs_from_txt) CONFIG_DEF(flag/forbid_singulo_possession) -CONFIG_DEF(flag/useircbot) //tgs2 support - protection = CONFIG_ENTRY_LOCKED - CONFIG_DEF(flag/automute_on) //enables automuting/spam prevention CONFIG_DEF(string/panic_server_name) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 6b17522ebe0..be5bedce6a4 100755 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -87,6 +87,8 @@ SUBSYSTEM_DEF(ticker) for(var/client/C in GLOB.clients) window_flash(C, ignorepref = TRUE) //let them know lobby has opened up. to_chat(world, "Welcome to [station_name()]!") + if(CONFIG_GET(flag/irc_announce_new_game)) + SERVER_TOOLS_CHAT_BROADCAST("New round starting on [SSmapping.config.map_name]!") current_state = GAME_STATE_PREGAME //Everyone who wants to be an observer is now spawned create_observers() diff --git a/code/datums/helper_datums/getrev.dm b/code/datums/helper_datums/getrev.dm index 4bee4c2c094..dfaa6c752a6 100644 --- a/code/datums/helper_datums/getrev.dm +++ b/code/datums/helper_datums/getrev.dm @@ -2,25 +2,10 @@ var/originmastercommit var/commit var/list/testmerge = list() - var/has_pr_details = FALSE //tgs2 support var/date /datum/getrev/New() - if(world.RunningService()) - var/file_name - if(ServiceVersion()) //will return null for versions < 3.0.91.0 - file_name = SERVICE_PR_TEST_JSON - else - file_name = SERVICE_PR_TEST_JSON_OLD - if(fexists(file_name)) - testmerge = json_decode(file2text(file_name)) -#ifdef SERVERTOOLS - else if(!world.RunningService() && fexists("../prtestjob.lk")) //tgs2 support - var/list/tmp = world.file2list("..\\prtestjob.lk") - for(var/I in tmp) - if(I) - testmerge |= I -#endif + testmerge = SERVER_TOOLS_PR_LIST log_world("Running /tg/ revision:") var/list/logs = world.file2list(".git/logs/HEAD") if(logs) @@ -36,53 +21,20 @@ log_world(commit) for(var/line in testmerge) if(line) - if(world.RunningService()) - var/tmcommit = testmerge[line]["commit"] - log_world("Test merge active of PR #[line] commit [tmcommit]") - SSblackbox.add_details("testmerged_prs","[line]|[tmcommit]") - else //tgs2 support - log_world("Test merge active of PR #[line]") - SSblackbox.add_details("testmerged_prs","[line]") + var/tmcommit = testmerge[line]["commit"] + log_world("Test merge active of PR #[line] commit [tmcommit]") + SSblackbox.add_details("testmerged_prs","[line]|[tmcommit]") log_world("Based off origin/master commit [originmastercommit]") else log_world(originmastercommit) -/datum/getrev/proc/DownloadPRDetails() - var/repo_id = CONFIG_GET(number/githubrepoid) - if(!repo_id) - if(testmerge.len) - log_world("PR details download failed: No github repo config set") - return - for(var/line in testmerge) - if(!isnum(text2num(line))) - log_world("PR details download failed: Invalid PR number: [line]") - return - - var/url = "https://api.github.com/repositories/[repo_id]/pulls/[line].json" - GLOB.valid_HTTPSGet = TRUE - var/json = HTTPSGet(url) - if(!json) - return - - testmerge[line] = json_decode(json) - - if(!testmerge[line]) - log_world("PR details download failed: null details returned") - return - CHECK_TICK - log_world("PR details successfully downloaded") - has_pr_details = TRUE /datum/getrev/proc/GetTestMergeInfo(header = TRUE) if(!testmerge.len) return "" . = header ? "The following pull requests are currently test merged:
" : "" for(var/line in testmerge) - var/details - if(world.RunningService()) - var/cm = testmerge[line]["commit"] - details = ": '" + html_encode(testmerge[line]["title"]) + "' by " + html_encode(testmerge[line]["author"]) + " at commit " + html_encode(copytext(cm, 1, min(length(cm), 7))) - else if(has_pr_details) //tgs2 support - details = ": '" + html_encode(testmerge[line]["title"]) + "' by " + html_encode(testmerge[line]["user"]["login"]) + var/cm = testmerge[line]["commit"] + var/details = ": '" + html_encode(testmerge[line]["title"]) + "' by " + html_encode(testmerge[line]["author"]) + " at commit " + html_encode(copytext(cm, 1, min(length(cm), 7))) if(details && findtext(details, "\[s\]") && (!usr || !usr.client.holder)) continue . += "#[line][details]
" diff --git a/code/game/world.dm b/code/game/world.dm index 316ed709617..65490a9a969 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -1,3 +1,6 @@ +#define PR_ANNOUNCEMENTS_PER_ROUND 5 //The number of unique PR announcements allowed per round + //This makes sure that a single person can only spam 3 reopens and 3 closes before being ignored + GLOBAL_VAR(security_mode) GLOBAL_PROTECT(security_mode) @@ -19,8 +22,7 @@ GLOBAL_PROTECT(security_mode) SetupLogs() - if(!RunningService()) //tgs2 support - GLOB.revdata.DownloadPRDetails() + SERVER_TOOLS_ON_NEW load_motd() load_admins() @@ -33,9 +35,6 @@ GLOBAL_PROTECT(security_mode) Master.Initialize(10, FALSE) - if(CONFIG_GET(flag/irc_announce_new_game)) - IRCBroadcast("New round starting on [SSmapping.config.map_name]!") - /world/proc/SetupExternalRSC() #if (PRELOAD_RSC == 0) external_rsc_urls = world.file2list("config/external_rsc_urls.txt","\n") @@ -125,8 +124,8 @@ GLOBAL_PROTECT(security_mode) if(!pinging && !playing && config && CONFIG_GET(flag/log_world_topic)) WRITE_FILE(GLOB.world_game_log, "TOPIC: \"[T]\", from:[addr], master:[master], key:[key]") - if(input[SERVICE_CMD_PARAM_KEY]) - return ServiceCommand(input) + SERVER_TOOLS_ON_TOPIC //redirect to server tools if necessary + var/comms_key = CONFIG_GET(string/comms_key) var/key_valid = (comms_key && input["key"] == comms_key) @@ -143,17 +142,6 @@ GLOBAL_PROTECT(security_mode) n++ return n - else if("ircstatus" in input) //tgs2 support - var/static/last_irc_status = 0 - if(world.time - last_irc_status < 50) - return - var/list/adm = get_admin_counts() - var/list/allmins = adm["total"] - var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). " - status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode.name]." - send2irc("Status", status) - last_irc_status = world.time - else if("status" in input) var/list/s = list() s["version"] = GLOB.game_version @@ -211,24 +199,6 @@ GLOBAL_PROTECT(security_mode) if(input["crossmessage"] == "News_Report") minor_announce(input["message"], "Breaking Update From [input["message_sender"]]") - else if("adminmsg" in input) //tgs2 support - if(!key_valid) - return "Bad Key" - else - return IrcPm(input["adminmsg"],input["msg"],input["sender"]) - - else if("namecheck" in input) //tgs2 support - if(!key_valid) - return "Bad Key" - else - log_admin("IRC Name Check: [input["sender"]] on [input["namecheck"]]") - message_admins("IRC name checking on [input["namecheck"]] from [input["sender"]]") - return keywords_lookup(input["namecheck"],1) - else if("adminwho" in input) //tgs2 support - if(!key_valid) - return "Bad Key" - else - return ircadminwho() else if("server_hop" in input) show_server_hop_transfer_screen(input["server_hop"]) @@ -247,7 +217,7 @@ GLOBAL_PROTECT(security_mode) C.AnnouncePR(final_composed) /world/Reboot(reason = 0, fast_track = FALSE) - ServiceReboot() //handles alternative actions if necessary + SERVER_TOOLS_ON_REBOOT if (reason || fast_track) //special reboot, do none of the normal stuff if (usr) log_admin("[key_name(usr)] Has requested an immediate world restart via client side debugging tools") diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 881e59204d9..a64cd8f40ea 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -421,8 +421,8 @@ return var/list/options = list("Regular Restart", "Hard Restart (No Delay/Feeback Reason)", "Hardest Restart (No actions, just reboot)") - if(world.RunningService()) - options += "Service Restart (Force restart DD)"; + if(SERVER_TOOLS_PRESENT) + options += "Server Restart (Kill and restart DD)"; var/rebootconfirm if(SSticker.admin_delay_notice) @@ -434,16 +434,19 @@ var/result = input(usr, "Select reboot method", "World Reboot", options[1]) as null|anything in options if(result) SSblackbox.add_details("admin_verb","Reboot World") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + var/init_by = "Initiated by [usr.client.holder.fakekey ? "Admin" : usr.key]." switch(result) if("Regular Restart") - SSticker.Reboot("Initiated by [usr.client.holder.fakekey ? "Admin" : usr.key].", "admin reboot - by [usr.key] [usr.client.holder.fakekey ? "(stealth)" : ""]", 10) + SSticker.Reboot(init_by, "admin reboot - by [usr.key] [usr.client.holder.fakekey ? "(stealth)" : ""]", 10) if("Hard Restart (No Delay, No Feeback Reason)") + to_chat(world, "World reboot - [init_by]") world.Reboot() if("Hardest Restart (No actions, just reboot)") + to_chat(world, "Hard world reboot - [init_by]") world.Reboot(fast_track = TRUE) - if("Service Restart (Force restart DD)") - GLOB.reboot_mode = REBOOT_MODE_HARD - world.ServiceReboot() + if("Server Restart (Kill and restart DD)") + to_chat(world, "Server restart - [init_by]") + SERVER_TOOLS_REBOOT_BYOND /datum/admins/proc/end_round() set category = "Server" diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm new file mode 100644 index 00000000000..e0d7f1315e8 --- /dev/null +++ b/code/modules/admin/chat_commands.dm @@ -0,0 +1,63 @@ +#define IRC_STATUS_THROTTLE 5 + +/datum/server_tools_command/ircstatus + name = "status" + help_text = "Gets the admincount, playercount, gamemode, and true game mode of the server" + admin_only = TRUE + var/static/last_irc_status = 0 + +/datum/server_tools_command/ircstatus/Run(sender, params) + var/rtod = REALTIMEOFDAY + if(rtod - last_irc_status < IRC_STATUS_THROTTLE) + return + last_irc_status = rtod + var/list/adm = get_admin_counts() + var/list/allmins = adm["total"] + var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). " + status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]." + return status + +/datum/server_tools_command/irccheck + name = "check" + help_text = "Gets the playercount, gamemode, and address of the server" + admin_only = TRUE + var/static/last_irc_check = 0 + +/datum/server_tools_command/irccheck/Run(sender, params) + var/rtod = REALTIMEOFDAY + if(rtod - last_irc_check < IRC_STATUS_THROTTLE) + return + last_irc_check = rtod + var/server = CONFIG_GET(string/server) + return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name], Mode: [GLOB.master_mode]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]" + +/datum/server_tools_command/ahelp + name = "ahelp" + help_text = " |list>>" + required_parameters = 2 + admin_only = TRUE + +/datum/server_tools_command/ahelp/Run(sender, params) + var/list/all_params = splittext(params, " ") + var/target = all_params[1] + all_params.Cut(1, 2) + return IrcPm(target, all_params.Join(" "), sender) + +/datum/server_tools_command/namecheck + name = "namecheck" + help_text = "Returns info on the specified target" + required_parameters = 1 + admin_only = TRUE + +/datum/server_tools_command/namecheck/Run(sender, params) + log_admin("IRC Name Check: [sender] on [params]") + message_admins("IRC name checking on [params] from [sender]") + return keywords_lookup(params, 1) + +/datum/server_tools_command/adminwho + name = "adminwho" + help_text = "Lists administrators currently on the server" + admin_only = TRUE + +/datum/server_tools_command/adminwho/Run(sender, params) + return ircadminwho() diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 25cfdba6f06..aa6c462d1f2 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -587,10 +587,8 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) /proc/send2irc(msg,msg2) - if(world.RunningService()) - world.ExportService("[SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE] [msg] | [msg2]") - else if(CONFIG_GET(flag/useircbot)) - shell("python nudge.py [msg] [msg2]") + if(SERVER_TOOLS_PRESENT) + SERVER_TOOLS_RELAY_BROADCAST("[msg] | [msg2]") /proc/send2otherserver(source,msg,type = "Ahelp") var/comms_key = CONFIG_GET(string/comms_key) diff --git a/code/modules/server_tools/nudge.py b/code/modules/server_tools/nudge.py deleted file mode 100644 index 4652d178cba..00000000000 --- a/code/modules/server_tools/nudge.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -import sys -import pickle -import socket - - -def pack(): - data = sys.argv[1] - - nudge(str.encode(data)) - - -def nudge(data): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - with open('config/server_to_tool_bridge_port.txt', 'r') as myfile: - portstr=myfile.read().replace('\n', '').strip() - s.connect(("localhost", int(portstr))) - s.send(data) - s.close() - -if __name__ == "__main__" and len(sys.argv) > 1: - pack() diff --git a/code/modules/server_tools/server_tools.dm b/code/modules/server_tools/server_tools.dm deleted file mode 100644 index 9cacb4542f2..00000000000 --- a/code/modules/server_tools/server_tools.dm +++ /dev/null @@ -1,93 +0,0 @@ -GLOBAL_VAR_INIT(reboot_mode, REBOOT_MODE_NORMAL) //if the world should request the service to kill it at reboot -GLOBAL_PROTECT(reboot_mode) - -/world/proc/RunningService() - return params[SERVICE_WORLD_PARAM] - -/proc/ServiceVersion() - if(world.RunningService()) - return world.params[SERVICE_VERSION_PARAM] - -/world/proc/ExportService(command) - return RunningService() && shell("python code/modules/server_tools/nudge.py \"[command]\"") == 0 - -/world/proc/IRCBroadcast(msg) - ExportService("[SERVICE_REQUEST_IRC_BROADCAST] [msg]") - -/world/proc/ServiceEndProcess() - log_world("Sending shutdown request!"); - sleep(1) //flush the buffers - ExportService(SERVICE_REQUEST_KILL_PROCESS) - -//called at the exact moment the world is supposed to reboot -/world/proc/ServiceReboot() - switch(GLOB.reboot_mode) - if(REBOOT_MODE_HARD) - to_chat(src, "Hard reboot triggered, you will automatically reconnect...") - ServiceEndProcess() - if(REBOOT_MODE_SHUTDOWN) - to_chat(src, "The server is shutting down...") - ServiceEndProcess() - else - ExportService(SERVICE_REQUEST_WORLD_REBOOT) //just let em know - -/world/proc/ServiceCommand(list/params) - var/sCK = RunningService() - var/their_sCK = params[SERVICE_CMD_PARAM_KEY] - - if(!their_sCK || their_sCK != sCK) - return "Invalid comms key!"; - - var/command = params[SERVICE_CMD_PARAM_COMMAND] - if(!command) - return "No command!" - - var/static/last_irc_status = 0 - switch(command) - if(SERVICE_CMD_HARD_REBOOT) - if(GLOB.reboot_mode != REBOOT_MODE_HARD) - GLOB.reboot_mode = REBOOT_MODE_HARD - log_world("Hard reboot requested by service") - message_admins("The world will hard reboot at the end of the game. Requested by service.") - SSblackbox.set_val("service_hard_restart", TRUE) - if(SERVICE_CMD_GRACEFUL_SHUTDOWN) - if(GLOB.reboot_mode != REBOOT_MODE_SHUTDOWN) - GLOB.reboot_mode = REBOOT_MODE_SHUTDOWN - log_world("Shutdown requested by service") - message_admins("The world will shutdown at the end of the game. Requested by service.") - SSblackbox.set_val("service_shutdown", TRUE) - if(SERVICE_CMD_WORLD_ANNOUNCE) - var/msg = params["message"] - if(!istext(msg) || !msg) - return "No message set!" - to_chat(src, "[html_encode(msg)]") - return "SUCCESS" - if(SERVICE_CMD_IRC_STATUS) - var/rtod = REALTIMEOFDAY - if(rtod - last_irc_status < IRC_STATUS_THROTTLE) - return - last_irc_status = rtod - var/list/adm = get_admin_counts() - var/list/allmins = adm["total"] - var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). " - status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]." - return status - if(SERVICE_CMD_IRC_CHECK) - var/rtod = REALTIMEOFDAY - if(rtod - last_irc_status < IRC_STATUS_THROTTLE) - return - last_irc_status = rtod - var/config_server = CONFIG_GET(string/server) - return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name], Mode: [GLOB.master_mode]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [config_server ? config_server : "[world.internet_address]:[world.port]"]" - if(SERVICE_CMD_ADMIN_MSG) - return IrcPm(params[SERVICE_CMD_PARAM_TARGET], params[SERVICE_CMD_PARAM_MESSAGE], params[SERVICE_CMD_PARAM_SENDER]) - - if(SERVICE_CMD_NAME_CHECK) - log_admin("IRC Name Check: [params[SERVICE_CMD_PARAM_SENDER]] on [params[SERVICE_CMD_PARAM_TARGET]]") - message_admins("IRC name checking on [params[SERVICE_CMD_PARAM_TARGET]] from [params[SERVICE_CMD_PARAM_SENDER]]") - return keywords_lookup(params[SERVICE_CMD_PARAM_TARGET], 1) - if(SERVICE_CMD_ADMIN_WHO) - return ircadminwho() - else - return "Unknown command: [command]" - diff --git a/code/modules/server_tools/st_commands.dm b/code/modules/server_tools/st_commands.dm new file mode 100644 index 00000000000..9ec87a595c6 --- /dev/null +++ b/code/modules/server_tools/st_commands.dm @@ -0,0 +1,76 @@ +/datum/server_tools_command + var/name = "" //the string to trigger this command on a chat bot. e.g. TGS3_BOT: do_this_command + var/help_text = "" //help text for this command + var/required_parameters = 0 //number of parameters required for this command + var/admin_only = FALSE //set to TRUE if this command should only be usable by registered chat admins + +//override to implement command +//sender is the display name of who sent the command +//params is the trimmed string following the command name +/datum/server_tools_command/proc/Run(sender, params) + CRASH("[type] has no implementation for Run()") + +/world/proc/ListServiceCustomCommands(warnings_only) + if(!warnings_only) + . = list() + var/list/command_name_types = list() + var/list/warned_command_names = warnings_only ? list() : null + for(var/I in typesof(/datum/server_tools_command) - /datum/server_tools_command) + var/datum/server_tools_command/stc = I + var/command_name = initial(stc.name) + var/static/list/warned_server_tools_names = list() + if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\"")) + if(warnings_only && !warned_command_names[command_name]) + SERVER_TOOLS_LOG("WARNING: Custom command [command_name] can't be used as it is empty or contains illegal characters!") + warned_command_names[command_name] = TRUE + continue + + if(command_name_types[command_name]) + if(warnings_only) + SERVER_TOOLS_LOG("WARNING: Custom commands [command_name_types[command_name]] and [stc] have the same name, only [command_name_types[command_name]] will be available!") + continue + command_name_types[stc] = command_name + + if(!warnings_only) + .[command_name] = list(SERVICE_JSON_PARAM_HELPTEXT = initial(stc.help_text), SERVICE_JSON_PARAM_ADMINONLY = initial(stc.admin_only), SERVICE_JSON_PARAM_REQUIREDPARAMETERS = initial(stc.required_parameters)) + +/world/proc/HandleServiceCustomCommand(command, sender, params) + var/static/list/cached_custom_server_tools_commands + if(!cached_custom_server_tools_commands) + cached_custom_server_tools_commands = list() + for(var/I in typesof(/datum/server_tools_command) - /datum/server_tools_command) + var/datum/server_tools_command/stc = I + cached_custom_server_tools_commands[lowertext(initial(stc.name))] = stc + + var/command_type = cached_custom_server_tools_commands[command] + if(!command_type) + return FALSE + var/datum/server_tools_command/stc = new command_type + return stc.Run(sender, params) || TRUE + +/* +The MIT License + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ diff --git a/code/modules/server_tools/st_interface.dm b/code/modules/server_tools/st_interface.dm new file mode 100644 index 00000000000..12fb5a85c28 --- /dev/null +++ b/code/modules/server_tools/st_interface.dm @@ -0,0 +1,125 @@ +SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(reboot_mode, REBOOT_MODE_NORMAL) +SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(server_tools_api_compatible, FALSE) + +/proc/GetTestMerges() + if(RunningService() && fexists(SERVICE_PR_TEST_JSON)) + . = json_decode(file2text(SERVICE_PR_TEST_JSON)) + if(.) + return + return list() + +/world/proc/ServiceInit() + if(!RunningService(TRUE)) + return + ListServiceCustomCommands(TRUE) + ExportService("[SERVICE_REQUEST_API_VERSION] [SERVER_TOOLS_API_VERSION]", TRUE) + +/proc/RunningService(skip_compat_check = FALSE) + return (skip_compat_check || SERVER_TOOLS_READ_GLOBAL(server_tools_api_compatible)) && world.params[SERVICE_WORLD_PARAM] != null + +/proc/ServiceVersion() + if(RunningService(TRUE)) + return world.params[SERVICE_VERSION_PARAM] + +/proc/ServiceAPIVersion() + return SERVICE_API_VERSION_STRING + +/world/proc/ExportService(command, skip_compat_check = FALSE) + . = FALSE + if(!RunningService(skip_compat_check)) + return + if(skip_compat_check && !fexists(SERVICE_INTERFACE_DLL)) + CRASH("Service parameter present but no interface DLL detected. This is symptomatic of running a service less than version 3.1! Please upgrade.") + call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(command) //trust no retval + return TRUE + +/world/proc/ChatBroadcast(message) + ExportService("[SERVICE_REQUEST_IRC_BROADCAST] [message]") + +/world/proc/AdminBroadcast(message) + ExportService("[SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE] [message]") + +/world/proc/ServiceEndProcess() + SERVER_TOOLS_LOG("Sending shutdown request!"); + sleep(world.tick_lag) //flush the buffers + ExportService(SERVICE_REQUEST_KILL_PROCESS) + +//called at the exact moment the world is supposed to reboot +/world/proc/ServiceReboot() + switch(SERVER_TOOLS_READ_GLOBAL(reboot_mode)) + if(REBOOT_MODE_HARD) + SERVER_TOOLS_WORLD_ANNOUNCE("Hard reboot triggered, you will automatically reconnect...") + ServiceEndProcess() + if(REBOOT_MODE_SHUTDOWN) + SERVER_TOOLS_WORLD_ANNOUNCE("The server is shutting down...") + ServiceEndProcess() + else + ExportService(SERVICE_REQUEST_WORLD_REBOOT) //just let em know + +/world/proc/ServiceCommand(list/params) + var/their_sCK = params[SERVICE_CMD_PARAM_KEY] + if(!their_sCK || !RunningService(TRUE)) + return FALSE //continue world/Topic + + var/sCK = world.params[SERVICE_WORLD_PARAM] + if(their_sCK != sCK) + return "Invalid comms key!"; + + var/command = params[SERVICE_CMD_PARAM_COMMAND] + if(!command) + return "No command!" + + switch(command) + if(SERVICE_CMD_API_COMPATIBLE) + SERVER_TOOLS_WRITE_GLOBAL(server_tools_api_compatible, TRUE) + return "SUCCESS" + if(SERVICE_CMD_HARD_REBOOT) + if(SERVER_TOOLS_READ_GLOBAL(reboot_mode) != REBOOT_MODE_HARD) + SERVER_TOOLS_WRITE_GLOBAL(reboot_mode, REBOOT_MODE_HARD) + SERVER_TOOLS_LOG("Hard reboot requested by service") + SERVER_TOOLS_NOTIFY_ADMINS("The world will hard reboot at the end of the game. Requested by service.") + if(SERVICE_CMD_GRACEFUL_SHUTDOWN) + if(SERVER_TOOLS_READ_GLOBAL(reboot_mode) != REBOOT_MODE_SHUTDOWN) + SERVER_TOOLS_WRITE_GLOBAL(reboot_mode, REBOOT_MODE_SHUTDOWN) + SERVER_TOOLS_LOG("Shutdown requested by service") + message_admins("The world will shutdown at the end of the game. Requested by service.") + if(SERVICE_CMD_WORLD_ANNOUNCE) + var/msg = params["message"] + if(!istext(msg) || !msg) + return "No message set!" + SERVER_TOOLS_WORLD_ANNOUNCE(msg) + return "SUCCESS" + if(SERVICE_CMD_LIST_CUSTOM) + return json_encode(ListServiceCustomCommands(FALSE)) + else + var/custom_command_result = HandleServiceCustomCommand(lowertext(command), params[SERVICE_CMD_PARAM_SENDER], params[SERVICE_CMD_PARAM_CUSTOM]) + if(custom_command_result) + return istext(custom_command_result) ? custom_command_result : "SUCCESS" + return "Unknown command: [command]" + +/* +The MIT License + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ diff --git a/config/server_to_tool_bridge_port.txt b/config/server_to_tool_bridge_port.txt deleted file mode 100644 index 75fd9c9d345..00000000000 --- a/config/server_to_tool_bridge_port.txt +++ /dev/null @@ -1 +0,0 @@ -45678 \ No newline at end of file diff --git a/tgstation.dme b/tgstation.dme index 7b8fd7797d3..d9d5c95e27b 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -1017,6 +1017,7 @@ #include "code\modules\admin\admin_verbs.dm" #include "code\modules\admin\adminmenu.dm" #include "code\modules\admin\banjob.dm" +#include "code\modules\admin\chat_commands.dm" #include "code\modules\admin\create_mob.dm" #include "code\modules\admin\create_object.dm" #include "code\modules\admin\create_poll.dm" @@ -2067,7 +2068,8 @@ #include "code\modules\ruins\spaceruin_code\TheDerelict.dm" #include "code\modules\security_levels\keycard_authentication.dm" #include "code\modules\security_levels\security_levels.dm" -#include "code\modules\server_tools\server_tools.dm" +#include "code\modules\server_tools\st_interface.dm" +#include "code\modules\server_tools\st_commands.dm" #include "code\modules\shuttle\arrivals.dm" #include "code\modules\shuttle\assault_pod.dm" #include "code\modules\shuttle\computer.dm"