From 08e6b8b441c92ac18b4db120764652746dede854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=83=D0=BC=D0=B0=D1=81=D0=B0=D0=BD=D0=B4=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=20C=D0=B0=D0=BC=D0=B0=D0=BB=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=91=D0=B0=D0=B1=D0=B0=D0=BD?= Date: Sun, 9 Jun 2024 21:54:03 +0700 Subject: [PATCH 1/2] Adds TGS --- README.md | 6 + code/__defines/tgs.config.dm | 12 + code/__defines/tgs.dm | 483 +++++++++++++++++++++++ code/_macros.dm | 48 +-- code/game/world.dm | 9 + code/modules/tgs/LICENSE | 24 ++ code/modules/tgs/README.md | 13 + code/modules/tgs/core/README.md | 9 + code/modules/tgs/core/_definitions.dm | 2 + code/modules/tgs/core/core.dm | 156 ++++++++ code/modules/tgs/core/datum.dm | 59 +++ code/modules/tgs/core/tgs_version.dm | 28 ++ code/modules/tgs/includes.dm | 18 + code/modules/tgs/v3210/README.md | 6 + code/modules/tgs/v3210/api.dm | 244 ++++++++++++ code/modules/tgs/v3210/commands.dm | 58 +++ code/modules/tgs/v4/README.md | 6 + code/modules/tgs/v4/api.dm | 312 +++++++++++++++ code/modules/tgs/v4/commands.dm | 44 +++ code/modules/tgs/v5/README.md | 10 + code/modules/tgs/v5/__interop_version.dm | 1 + code/modules/tgs/v5/_defines.dm | 100 +++++ code/modules/tgs/v5/api.dm | 378 ++++++++++++++++++ code/modules/tgs/v5/commands.dm | 60 +++ code/modules/tgs/v5/serializers.dm | 52 +++ code/modules/tgs/v5/undefs.dm | 100 +++++ nebula.dme | 3 + tools/validate_dme.py | 2 + 28 files changed, 2221 insertions(+), 22 deletions(-) create mode 100644 code/__defines/tgs.config.dm create mode 100644 code/__defines/tgs.dm create mode 100644 code/modules/tgs/LICENSE create mode 100644 code/modules/tgs/README.md create mode 100644 code/modules/tgs/core/README.md create mode 100644 code/modules/tgs/core/_definitions.dm create mode 100644 code/modules/tgs/core/core.dm create mode 100644 code/modules/tgs/core/datum.dm create mode 100644 code/modules/tgs/core/tgs_version.dm create mode 100644 code/modules/tgs/includes.dm create mode 100644 code/modules/tgs/v3210/README.md create mode 100644 code/modules/tgs/v3210/api.dm create mode 100644 code/modules/tgs/v3210/commands.dm create mode 100644 code/modules/tgs/v4/README.md create mode 100644 code/modules/tgs/v4/api.dm create mode 100644 code/modules/tgs/v4/commands.dm create mode 100644 code/modules/tgs/v5/README.md create mode 100644 code/modules/tgs/v5/__interop_version.dm create mode 100644 code/modules/tgs/v5/_defines.dm create mode 100644 code/modules/tgs/v5/api.dm create mode 100644 code/modules/tgs/v5/commands.dm create mode 100644 code/modules/tgs/v5/serializers.dm create mode 100644 code/modules/tgs/v5/undefs.dm diff --git a/README.md b/README.md index 3d4d64d1569..0e7aef1a082 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,12 @@ Code with a git authorship date prior to `1420675200 +0000` (2015/01/08 00:00 GM All code where the authorship dates on or after `1420675200 +0000` is assumed to be licensed under AGPL v3, if you wish to license under GPL v3 please make this clear in the commit message and any added files. + +The TGS DMAPI API is licensed as a subproject under the MIT license. + +See the footer of [code/__defines/tgs.dm](./code/__DEFINES/tgs.dm) and [code/modules/tgs/LICENSE](./code/modules/tgs/LICENSE) for the MIT license. + + If you wish to develop and host this codebase in a closed source manner you may use all commits prior to `1420675200 +0000`, which are licensed under GPL v3. The major change here is that if you host a server using any code licensed under AGPLv3 you are required to provide full source code for your servers users as well including addons and modifications you have made. See [here](https://www.gnu.org/licenses/why-affero-gpl.html) for more information. diff --git a/code/__defines/tgs.config.dm b/code/__defines/tgs.config.dm new file mode 100644 index 00000000000..a90ee2672b1 --- /dev/null +++ b/code/__defines/tgs.config.dm @@ -0,0 +1,12 @@ +#define TGS_EXTERNAL_CONFIGURATION +//#define TGS_V3_API +#define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) var/global/##Name = ##Value +#define TGS_READ_GLOBAL(Name) global.##Name +#define TGS_WRITE_GLOBAL(Name, Value) global.##Name = ##Value +#define TGS_WORLD_ANNOUNCE(message) to_chat(world, SPAN_BOLDANNOUNCE([html_encode(##message)])) +#define TGS_INFO_LOG(message) log_world("TGS Info: [##message]") +#define TGS_WARNING_LOG(message) log_world("TGS Warn: [##message]") +#define TGS_ERROR_LOG(message) log_world("TGS Error: [##message]") +#define TGS_NOTIFY_ADMINS(event) message_admins(##event) +#define TGS_CLIENT_COUNT global.clients.len +#define TGS_PROTECT_DATUM(Path) \ No newline at end of file diff --git a/code/__defines/tgs.dm b/code/__defines/tgs.dm new file mode 100644 index 00000000000..3744a95a0f8 --- /dev/null +++ b/code/__defines/tgs.dm @@ -0,0 +1,483 @@ +// tgstation-server DMAPI + +#define TGS_DMAPI_VERSION "6.2.0" + +// All functions and datums outside this document are subject to change with any version and should not be relied on. + +// CONFIGURATION + +/// Create this define if you want to do TGS configuration outside of this file. +#ifndef TGS_EXTERNAL_CONFIGURATION + +// Comment this out once you've filled in the below. +#error TGS API unconfigured + +// Uncomment this if you wish to allow the game to interact with TGS 3. +// This will raise the minimum required security level of your game to TGS_SECURITY_TRUSTED due to it utilizing call()() +//#define TGS_V3_API + +// Required interfaces (fill in with your codebase equivalent): + +/// Create a global variable named `Name` and set it to `Value`. +#define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) + +/// Read the value in the global variable `Name`. +#define TGS_READ_GLOBAL(Name) + +/// Set the value in the global variable `Name` to `Value`. +#define TGS_WRITE_GLOBAL(Name, Value) + +/// Disallow ANYONE from reflecting a given `path`, security measure to prevent in-game use of DD -> TGS capabilities. +#define TGS_PROTECT_DATUM(Path) + +/// Display an announcement `message` from the server to all players. +#define TGS_WORLD_ANNOUNCE(message) + +/// Notify current in-game administrators of a string `event`. +#define TGS_NOTIFY_ADMINS(event) + +/// Write an info `message` to a server log. +#define TGS_INFO_LOG(message) + +/// Write an warning `message` to a server log. +#define TGS_WARNING_LOG(message) + +/// Write an error `message` to a server log. +#define TGS_ERROR_LOG(message) + +/// Get the number of connected /clients. +#define TGS_CLIENT_COUNT + +#endif + +// EVENT CODES + +/// Before a reboot mode change, extras parameters are the current and new reboot mode enums +#define TGS_EVENT_REBOOT_MODE_CHANGE -1 +/// Before a port change is about to happen, extra parameters is new port +#define TGS_EVENT_PORT_SWAP -2 +/// Before the instance is renamed, extra parameter is the new name +#define TGS_EVENT_INSTANCE_RENAMED -3 +/// After the watchdog reattaches to DD, extra parameter is the new [/datum/tgs_version] of the server +#define TGS_EVENT_WATCHDOG_REATTACH -4 + +/// When the repository is reset to its origin reference. Parameters: Reference name, Commit SHA +#define TGS_EVENT_REPO_RESET_ORIGIN 0 +/// When the repository performs a checkout. Parameters: Checkout git object +#define TGS_EVENT_REPO_CHECKOUT 1 +/// When the repository performs a fetch operation. No parameters +#define TGS_EVENT_REPO_FETCH 2 +/// When the repository test merges. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user +#define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3 +/// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path +#define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4 +/// Before a BYOND install operation begins. Parameters: [/datum/tgs_version] of the installing BYOND +#define TGS_EVENT_BYOND_INSTALL_START 5 +/// When a BYOND install operation fails. Parameters: Error message +#define TGS_EVENT_BYOND_INSTALL_FAIL 6 +/// When the active BYOND version changes. Parameters: (Nullable) [/datum/tgs_version] of the current BYOND, [/datum/tgs_version] of the new BYOND +#define TGS_EVENT_BYOND_ACTIVE_VERSION_CHANGE 7 +/// When the compiler starts running. Parameters: Game directory path, origin commit SHA +#define TGS_EVENT_COMPILE_START 8 +/// When a compile is cancelled. No parameters +#define TGS_EVENT_COMPILE_CANCELLED 9 +/// When a compile fails. Parameters: Game directory path, [TRUE]/[FALSE] based on if the cause for failure was DMAPI validation +#define TGS_EVENT_COMPILE_FAILURE 10 +/// When a compile operation completes. Note, this event fires before the new .dmb is loaded into the watchdog. Consider using the [TGS_EVENT_DEPLOYMENT_COMPLETE] instead. Parameters: Game directory path +#define TGS_EVENT_COMPILE_COMPLETE 11 +/// When an automatic update for the current instance begins. No parameters +#define TGS_EVENT_INSTANCE_AUTO_UPDATE_START 12 +/// When the repository encounters a merge conflict: Parameters: Base SHA, target SHA, base reference, target reference +#define TGS_EVENT_REPO_MERGE_CONFLICT 13 +/// When a deployment completes. No Parameters +#define TGS_EVENT_DEPLOYMENT_COMPLETE 14 +/// Before the watchdog shuts down. Not sent for graceful shutdowns. No parameters. +#define TGS_EVENT_WATCHDOG_SHUTDOWN 15 +/// Before the watchdog detaches for a TGS update/restart. No parameters. +#define TGS_EVENT_WATCHDOG_DETACH 16 +// We don't actually implement these 4 events as the DMAPI can never receive them. +// #define TGS_EVENT_WATCHDOG_LAUNCH 17 +// #define TGS_EVENT_WATCHDOG_CRASH 18 +// #define TGS_EVENT_WORLD_END_PROCESS 19 +// #define TGS_EVENT_WORLD_REBOOT 20 +/// Watchdog event when TgsInitializationComplete() is called. No parameters. +#define TGS_EVENT_WORLD_PRIME 21 +// DMAPI also doesnt implement this +// #define TGS_EVENT_DREAM_DAEMON_LAUNCH 22 + +// OTHER ENUMS + +/// The server will reboot normally. +#define TGS_REBOOT_MODE_NORMAL 0 +/// The server will stop running on reboot. +#define TGS_REBOOT_MODE_SHUTDOWN 1 +/// The watchdog will restart on reboot. +#define TGS_REBOOT_MODE_RESTART 2 + +/// DreamDaemon Trusted security level. +#define TGS_SECURITY_TRUSTED 0 +/// DreamDaemon Safe security level. +#define TGS_SECURITY_SAFE 1 +/// DreamDaemon Ultrasafe security level. +#define TGS_SECURITY_ULTRASAFE 2 + +//REQUIRED HOOKS + +/** + * Call this somewhere in [/world/proc/New] that is always run. This function may sleep! + * + * * event_handler - Optional user defined [/datum/tgs_event_handler]. + * * minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated. Can be one of [TGS_SECURITY_ULTRASAFE], [TGS_SECURITY_SAFE], or [TGS_SECURITY_TRUSTED]. + */ +/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE) + return + +/** + * Call this when your initializations are complete and your game is ready to play before any player interactions happen. + * + * This may use [/world/var/sleep_offline] to make this happen so ensure no changes are made to it while this call is running. + * Afterwards, consider explicitly setting it to what you want to avoid this BYOND bug: http://www.byond.com/forum/post/2575184 + * This function should not be called before ..() in [/world/proc/New]. + */ +/world/proc/TgsInitializationComplete() + return + +/// Put this at the start of [/world/proc/Topic]. +#define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return + +/** + * Call this as late as possible in [world/proc/Reboot]. + */ +/world/proc/TgsReboot() + return + +// DATUM DEFINITIONS +// All datums defined here should be considered read-only + +/// Represents git revision information. +/datum/tgs_revision_information + /// Full SHA of the commit. + var/commit + /// ISO 8601 timestamp of when the commit was created + var/timestamp + /// Full sha of last known remote commit. This may be null if the TGS repository is not currently tracking a remote branch. + var/origin_commit + +/// Represents a version. +/datum/tgs_version + /// The suite/major version number + var/suite + + // This group of variables can be null to represent a wild card + /// The minor version number. null for wildcards + var/minor + /// The patch version number. null for wildcards + var/patch + + /// Legacy version number. Generally null + var/deprecated_patch + + /// Unparsed string value + var/raw_parameter + /// String value minus prefix + var/deprefixed_parameter + +/** + * Returns [TRUE]/[FALSE] based on if the [/datum/tgs_version] contains wildcards. + */ +/datum/tgs_version/proc/Wildcard() + return + +/** + * Returns [TRUE]/[FALSE] based on if the [/datum/tgs_version] equals some other version. + * + * other_version - The [/datum/tgs_version] to compare against. + */ +/datum/tgs_version/proc/Equals(datum/tgs_version/other_version) + return + +/// Represents a merge of a GitHub pull request. +/datum/tgs_revision_information/test_merge + /// The test merge number. + var/number + /// The test merge source's title when it was merged. + var/title + /// The test merge source's body when it was merged. + var/body + /// The Username of the test merge source's author. + var/author + /// An http URL to the test merge source. + var/url + /// The SHA of the test merge when that was merged. + var/head_commit + /// Optional comment left by the TGS user who initiated the merge. + var/comment + +/// Represents a connected chat channel. +/datum/tgs_chat_channel + /// TGS internal channel ID. + var/id + /// User friendly name of the channel. + var/friendly_name + /// Name of the chat connection. This is the IRC server address or the Discord guild. + var/connection_name + /// [TRUE]/[FALSE] based on if the server operator has marked this channel for game admins only. + var/is_admin_channel + /// [TRUE]/[FALSE] if the channel is a private message channel for a [/datum/tgs_chat_user]. + var/is_private_channel + /// Tag string associated with the channel in TGS + var/custom_tag + /// [TRUE]/[FALSE] if the channel supports embeds + var/embeds_supported + +// Represents a chat user +/datum/tgs_chat_user + /// TGS internal user ID. + var/id + // The user's display name. + var/friendly_name + // The string to use to ping this user in a message. + var/mention + /// The [/datum/tgs_chat_channel] the user was from + var/datum/tgs_chat_channel/channel + +/** + * User definable callback for handling TGS events. + * + * event_code - One of the TGS_EVENT_ defines. Extra parameters will be documented in each + */ +/datum/tgs_event_handler/proc/HandleEvent(event_code, ...) + set waitfor = FALSE + return + +/// User definable chat command +/datum/tgs_chat_command + /// The string to trigger this command on a chat bot. e.g `@bot name ...` or `!tgs name ...` + var/name = "" + /// The help text displayed for this command + var/help_text = "" + /// If this command should be available to game administrators only + var/admin_only = FALSE + /// A subtype of [/datum/tgs_chat_command] that is ignored when enumerating available commands. Use this to create shared base /datums for commands. + var/ignore_type + +/** + * Process command activation. Should return a [/datum/tgs_message_content] to respond to the issuer with. + * + * sender - The [/datum/tgs_chat_user] who issued the command. + * params - The trimmed string following the command `/datum/tgs_chat_command/var/name]. + */ +/datum/tgs_chat_command/proc/Run(datum/tgs_chat_user/sender, params) + CRASH("[type] has no implementation for Run()") + +/// User definable chat message +/datum/tgs_message_content + /// The tring content of the message. Must be provided in New(). + var/text + + /// The [/datum/tgs_chat_embed] to embed in the message. Not supported on all chat providers. + var/datum/tgs_chat_embed/structure/embed + +/datum/tgs_message_content/New(text) + if(!istext(text)) + TGS_ERROR_LOG("[/datum/tgs_message_content] created with no text!") + text = null + + src.text = text + +/// User definable chat embed. Currently mirrors Discord chat embeds. See https://discord.com/developers/docs/resources/channel#embed-object-embed-structure for details. +/datum/tgs_chat_embed/structure + var/title + var/description + var/url + + /// Timestamp must be encoded as: time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss"). Use the active timezone. + var/timestamp + + /// Colour must be #AARRGGBB or #RRGGBB hex string + var/colour + + /// See https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure for details. + var/datum/tgs_chat_embed/media/image + + /// See https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure for details. + var/datum/tgs_chat_embed/media/thumbnail + + /// See https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure for details. + var/datum/tgs_chat_embed/media/video + + var/datum/tgs_chat_embed/footer/footer + var/datum/tgs_chat_embed/provider/provider + var/datum/tgs_chat_embed/provider/author/author + + var/list/datum/tgs_chat_embed/field/fields + +/// Common datum for similar discord embed medias +/datum/tgs_chat_embed/media + /// Must be set in New(). + var/url + var/width + var/height + var/proxy_url + +/datum/tgs_chat_embed/media/New(url) + if(!istext(url)) + CRASH("[/datum/tgs_chat_embed/media] created with no url!") + + src.url = url + +/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure for details. +/datum/tgs_chat_embed/footer + /// Must be set in New(). + var/text + var/icon_url + var/proxy_icon_url + +/datum/tgs_chat_embed/footer/New(text) + if(!istext(text)) + CRASH("[/datum/tgs_chat_embed/footer] created with no text!") + + src.text = text + +/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-provider-structure for details. +/datum/tgs_chat_embed/provider + var/name + var/url + +/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure for details. Must have name set in New(). +/datum/tgs_chat_embed/provider/author + var/icon_url + var/proxy_icon_url + +/datum/tgs_chat_embed/provider/author/New(name) + if(!istext(name)) + CRASH("[/datum/tgs_chat_embed/provider/author] created with no name!") + + src.name = name + +/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure for details. Must have name and value set in New(). +/datum/tgs_chat_embed/field + var/name + var/value + var/is_inline + +/datum/tgs_chat_embed/field/New(name, value) + if(!istext(name)) + CRASH("[/datum/tgs_chat_embed/field] created with no name!") + + if(!istext(value)) + CRASH("[/datum/tgs_chat_embed/field] created with no value!") + + src.name = name + src.value = value + +// API FUNCTIONS + +/// Returns the maximum supported [/datum/tgs_version] of the DMAPI. +/world/proc/TgsMaximumApiVersion() + return + +/// Returns the minimum supported [/datum/tgs_version] of the DMAPI. +/world/proc/TgsMinimumApiVersion() + return + +/** + * Returns [TRUE] if DreamDaemon was launched under TGS, the API matches, and was properly initialized. [FALSE] will be returned otherwise. + */ +/world/proc/TgsAvailable() + return + +// No function below this succeeds if it TgsAvailable() returns FALSE or if TgsNew() has yet to be called. + +/** + * Forces a hard reboot of DreamDaemon by ending the process. + * + * Unlike del(world) clients will try to reconnect. + * If TGS has not requested a [TGS_REBOOT_MODE_SHUTDOWN] DreamDaemon will be launched again + */ +/world/proc/TgsEndProcess() + return + +/** + * Send a message to connected chats. + * + * message - The [/datum/tgs_message_content] to send. + * admin_only: If [TRUE], message will be sent to admin connected chats. Vice-versa applies. + */ +/world/proc/TgsTargetedChatBroadcast(datum/tgs_message_content/message, admin_only = FALSE) + return + +/** + * Send a private message to a specific user. + * + * message - The [/datum/tgs_message_content] to send. + * user: The [/datum/tgs_chat_user] to PM. + */ +/world/proc/TgsChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user) + return + +// The following functions will sleep if a call to TgsNew() is sleeping + +/** + * Send a message to connected chats that are flagged as game-related in TGS. + * + * message - The [/datum/tgs_message_content] to send. + * channels - Optional list of [/datum/tgs_chat_channel]s to restrict the message to. + */ +/world/proc/TgsChatBroadcast(datum/tgs_message_content/message, list/channels = null) + return + +/// Returns the current [/datum/tgs_version] of TGS if it is running the server, null otherwise. +/world/proc/TgsVersion() + return + +/// Returns the current [/datum/tgs_version] of the DMAPI being used if it was activated, null otherwise. +/world/proc/TgsApiVersion() + return + +/// Returns the name of the TGS instance running the game if TGS is present, null otherwise. +/world/proc/TgsInstanceName() + return + +/// Return the current [/datum/tgs_revision_information] of the running server if TGS is present, null otherwise. +/world/proc/TgsRevision() + return + +/// Returns the current BYOND security level as a TGS_SECURITY_ define if TGS is present, null otherwise. +/world/proc/TgsSecurityLevel() + return + +/// Returns a list of active [/datum/tgs_revision_information/test_merge]s if TGS is present, null otherwise. +/world/proc/TgsTestMerges() + return + +/// Returns a list of connected [/datum/tgs_chat_channel]s if TGS is present, null otherwise. +/world/proc/TgsChatChannelInfo() + return + +/* +The MIT License + +Copyright (c) 2017 Jordan Brown + +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/_macros.dm b/code/_macros.dm index 31458da86e5..e72b41251c5 100644 --- a/code/_macros.dm +++ b/code/_macros.dm @@ -146,29 +146,33 @@ #define SPAN_STYLE(S, X) "[X]" +// SS220 ADD BEGIN #define SPAN_CLASS(C, X) "[X]" -#define SPAN_ITALIC(X) SPAN_CLASS("italic", X) -#define SPAN_BOLD(X) SPAN_CLASS("bold", X) -#define SPAN_NOTICE(X) SPAN_CLASS("notice", X) -#define SPAN_WARNING(X) SPAN_CLASS("warning", X) -#define SPAN_DANGER(X) SPAN_CLASS("danger", X) -#define SPAN_ROSE(X) SPAN_CLASS("rose", X) -#define SPAN_OCCULT(X) SPAN_CLASS("cult", X) -#define SPAN_MFAUNA(X) SPAN_CLASS("mfauna", X) -#define SPAN_SUBTLE(X) SPAN_CLASS("subtle", X) -#define SPAN_INFO(X) SPAN_CLASS("info", X) -#define SPAN_RED(X) SPAN_CLASS("font_red", X) -#define SPAN_ORANGE(X) SPAN_CLASS("font_orange", X) -#define SPAN_YELLOW(X) SPAN_CLASS("font_yellow", X) -#define SPAN_GREEN(X) SPAN_CLASS("font_green", X) -#define SPAN_BLUE(X) SPAN_CLASS("font_blue", X) -#define SPAN_VIOLET(X) SPAN_CLASS("font_violet", X) -#define SPAN_PURPLE(X) SPAN_CLASS("font_purple", X) -#define SPAN_GREY(X) SPAN_CLASS("font_grey", X) -#define SPAN_MAROON(X) SPAN_CLASS("font_maroon", X) -#define SPAN_PINK(X) SPAN_CLASS("font_pink", X) -#define SPAN_PALEPINK(X) SPAN_CLASS("font_palepink", X) -#define SPAN_SINISTER(X) SPAN_CLASS("sinister", X) +#define SPAN_ITALIC(X) SPAN_CLASS("italic", X) +#define SPAN_BOLD(X) SPAN_CLASS("bold", X) +#define SPAN_BOLDANNOUNCE(X) SPAN_CLASS("boldannounce", X) +#define SPAN_NOTICE(X) SPAN_CLASS("notice", X) +#define SPAN_WARNING(X) SPAN_CLASS("warning", X) +#define SPAN_DANGER(X) SPAN_CLASS("danger", X) +#define SPAN_ROSE(X) SPAN_CLASS("rose", X) +#define SPAN_OCCULT(X) SPAN_CLASS("cult", X) +#define SPAN_MFAUNA(X) SPAN_CLASS("mfauna", X) +#define SPAN_SUBTLE(X) SPAN_CLASS("subtle", X) +#define SPAN_INFO(X) SPAN_CLASS("info", X) +#define SPAN_RED(X) SPAN_CLASS("font_red", X) +#define SPAN_ORANGE(X) SPAN_CLASS("font_orange", X) +#define SPAN_YELLOW(X) SPAN_CLASS("font_yellow", X) +#define SPAN_GREEN(X) SPAN_CLASS("font_green", X) +#define SPAN_BLUE(X) SPAN_CLASS("font_blue", X) +#define SPAN_VIOLET(X) SPAN_CLASS("font_violet", X) +#define SPAN_PURPLE(X) SPAN_CLASS("font_purple", X) +#define SPAN_GREY(X) SPAN_CLASS("font_grey", X) +#define SPAN_MAROON(X) SPAN_CLASS("font_maroon", X) +#define SPAN_PINK(X) SPAN_CLASS("font_pink", X) +#define SPAN_PALEPINK(X) SPAN_CLASS("font_palepink", X) +#define SPAN_SINISTER(X) SPAN_CLASS("sinister", X) +// SS220 ADD END + // placeholders #define SPAN_GOOD(X) SPAN_GREEN(X) #define SPAN_NEUTRAL(X) SPAN_BLUE(X) diff --git a/code/game/world.dm b/code/game/world.dm index f5b8dc6d4a7..c666e3b3cf4 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -78,6 +78,11 @@ GLOBAL_PROTECTED_UNTYPED(game_id, null) //logs SetupLogs() + // SS220 ADD BEGIN + TgsNew() + TgsInitializationComplete() + // SS220 ADD END + changelog_hash = md5('html/changelog.html') //used for telling if the changelog has changed recently if(byond_version < REQUIRED_DM_VERSION) @@ -111,6 +116,8 @@ var/global/world_topic_last = world.timeofday /world/Topic(T, addr, master, key) direct_output(diary, "TOPIC: \"[T]\", from:[addr], master:[master], key:[key][log_end]") + TGS_TOPIC + if (global.world_topic_last > world.timeofday) global.world_topic_throttle = list() //probably passed midnight global.world_topic_last = world.timeofday @@ -135,6 +142,8 @@ var/global/world_topic_last = world.timeofday global.Master.restart_timeout = 5 MINUTES return + TgsReboot() + if(global.using_map.reboot_sound) sound_to(world, sound(pick(global.using_map.reboot_sound)))// random end sounds!! - LastyBatsy diff --git a/code/modules/tgs/LICENSE b/code/modules/tgs/LICENSE new file mode 100644 index 00000000000..221f9e1deb2 --- /dev/null +++ b/code/modules/tgs/LICENSE @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) 2017 Jordan Brown + +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/tgs/README.md b/code/modules/tgs/README.md new file mode 100644 index 00000000000..6319028d810 --- /dev/null +++ b/code/modules/tgs/README.md @@ -0,0 +1,13 @@ +# DMAPI Internals + +This folder should be placed on it's own inside a codebase that wishes to use the TGS DMAPI. Warranty void if modified. + +- [includes.dm](./includes.dm) is the file that should be included by DM code, it handles including the rest. +- The [core](./core) folder includes all code not directly part of any API version. +- The other versioned folders contain code for the different DMAPI versions. + - [v3210](./v3210) contains the final TGS3 API. + - [v4](./v4) is the legacy DMAPI 4 (Used in TGS 4.0.X versions). + - [v5](./v5) is the current DMAPI version used by TGS >=4.1. +- [LICENSE](./LICENSE) is the MIT license for the DMAPI. + +APIs communicate with TGS in two ways. All versions implement TGS -> DM communication using /world/Topic. DM -> TGS communication, called the bridge method, is different for each version. diff --git a/code/modules/tgs/core/README.md b/code/modules/tgs/core/README.md new file mode 100644 index 00000000000..b82d8f49e29 --- /dev/null +++ b/code/modules/tgs/core/README.md @@ -0,0 +1,9 @@ +# Core DMAPI functions + +This folder contains all DMAPI code not directly involved in an API. + +- [_definitions.dm](./definitions.dm) contains defines needed across DMAPI internals. +- [core.dm](./core.dm) contains the implementations of the `/world/proc/TgsXXX()` procs. Many map directly to the `/datum/tgs_api` functions. It also contains the /datum selection and setup code. +- [datum.dm](./datum.dm) contains the `/datum/tgs_api` declarations that all APIs must implement. +- [tgs_version.dm](./tgs_version.dm) contains the `/datum/tgs_version` definition +- diff --git a/code/modules/tgs/core/_definitions.dm b/code/modules/tgs/core/_definitions.dm new file mode 100644 index 00000000000..ebf6d17c2a0 --- /dev/null +++ b/code/modules/tgs/core/_definitions.dm @@ -0,0 +1,2 @@ +#define TGS_UNIMPLEMENTED "___unimplemented" +#define TGS_VERSION_PARAMETER "server_service_version" diff --git a/code/modules/tgs/core/core.dm b/code/modules/tgs/core/core.dm new file mode 100644 index 00000000000..41a04733945 --- /dev/null +++ b/code/modules/tgs/core/core.dm @@ -0,0 +1,156 @@ +/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE) + var/current_api = TGS_READ_GLOBAL(tgs) + if(current_api) + TGS_ERROR_LOG("API datum already set (\ref[current_api] ([current_api]))! Was TgsNew() called more than once?") + return + + if(!(minimum_required_security_level in list(TGS_SECURITY_ULTRASAFE, TGS_SECURITY_SAFE, TGS_SECURITY_TRUSTED))) + TGS_ERROR_LOG("Invalid minimum_required_security_level: [minimum_required_security_level]!") + return + +#ifdef TGS_V3_API + if(minimum_required_security_level != TGS_SECURITY_TRUSTED) + TGS_WARNING_LOG("V3 DMAPI requires trusted security!") + minimum_required_security_level = TGS_SECURITY_TRUSTED +#endif + var/raw_parameter = world.params[TGS_VERSION_PARAMETER] + if(!raw_parameter) + return + + var/datum/tgs_version/version = new(raw_parameter) + if(!version.Valid(FALSE)) + TGS_ERROR_LOG("Failed to validate DMAPI version parameter: [raw_parameter]!") + return + + var/api_datum + switch(version.suite) + if(3) +#ifndef TGS_V3_API + TGS_ERROR_LOG("Detected V3 API but TGS_V3_API isn't defined!") + return +#else + switch(version.minor) + if(2) + api_datum = /datum/tgs_api/v3210 +#endif + if(4) + switch(version.minor) + if(0) + api_datum = /datum/tgs_api/v4 + if(5) + api_datum = /datum/tgs_api/v5 + + var/datum/tgs_version/max_api_version = TgsMaximumApiVersion(); + if(version.suite != null && version.minor != null && version.patch != null && version.deprecated_patch != null && version.deprefixed_parameter > max_api_version.deprefixed_parameter) + TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.") + api_datum = /datum/tgs_api/latest + + if(!api_datum) + TGS_ERROR_LOG("Found unsupported API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.") + return + + TGS_INFO_LOG("Activating API for version [version.deprefixed_parameter]") + + if(event_handler && !istype(event_handler)) + TGS_ERROR_LOG("Invalid parameter for event_handler: [event_handler]") + event_handler = null + + var/datum/tgs_api/new_api = new api_datum(event_handler, version) + + TGS_WRITE_GLOBAL(tgs, new_api) + + var/result = new_api.OnWorldNew(minimum_required_security_level) + if(!result || result == TGS_UNIMPLEMENTED) + TGS_WRITE_GLOBAL(tgs, null) + TGS_ERROR_LOG("Failed to activate API!") + +/world/TgsMaximumApiVersion() + return new /datum/tgs_version("5.x.x") + +/world/TgsMinimumApiVersion() + return new /datum/tgs_version("3.2.x") + +/world/TgsInitializationComplete() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + api.OnInitializationComplete() + +/world/proc/TgsTopic(T) + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + var/result = api.OnTopic(T) + if(result != TGS_UNIMPLEMENTED) + return result + +/world/TgsRevision() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + var/result = api.Revision() + if(result != TGS_UNIMPLEMENTED) + return result + +/world/TgsReboot() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + api.OnReboot() + +/world/TgsAvailable() + return TGS_READ_GLOBAL(tgs) != null + +/world/TgsVersion() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + return api.version + +/world/TgsApiVersion() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + return api.ApiVersion() + +/world/TgsInstanceName() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + var/result = api.InstanceName() + if(result != TGS_UNIMPLEMENTED) + return result + +/world/TgsTestMerges() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + var/result = api.TestMerges() + if(result != TGS_UNIMPLEMENTED) + return result + return list() + +/world/TgsEndProcess() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + api.EndProcess() + +/world/TgsChatChannelInfo() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + var/result = api.ChatChannelInfo() + if(result != TGS_UNIMPLEMENTED) + return result + return list() + +/world/TgsChatBroadcast(message, list/channels) + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + api.ChatBroadcast(message, channels) + +/world/TgsTargetedChatBroadcast(message, admin_only) + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + api.ChatTargetedBroadcast(message, admin_only) + +/world/TgsChatPrivateMessage(message, datum/tgs_chat_user/user) + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + api.ChatPrivateMessage(message, user) + +/world/TgsSecurityLevel() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + api.SecurityLevel() diff --git a/code/modules/tgs/core/datum.dm b/code/modules/tgs/core/datum.dm new file mode 100644 index 00000000000..68b0330fe86 --- /dev/null +++ b/code/modules/tgs/core/datum.dm @@ -0,0 +1,59 @@ +TGS_DEFINE_AND_SET_GLOBAL(tgs, null) + +/datum/tgs_api + var/datum/tgs_version/version + var/datum/tgs_event_handler/event_handler + + var/list/warned_deprecated_command_runs + +/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version) + . = ..() + src.event_handler = event_handler + src.version = version + +/datum/tgs_api/latest + parent_type = /datum/tgs_api/v5 + +TGS_PROTECT_DATUM(/datum/tgs_api) + +/datum/tgs_api/proc/ApiVersion() + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/OnWorldNew(datum/tgs_event_handler/event_handler) + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/OnInitializationComplete() + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/OnTopic(T) + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/OnReboot() + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/InstanceName() + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/TestMerges() + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/EndProcess() + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/Revision() + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/ChatChannelInfo() + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/ChatBroadcast(message, list/channels) + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/ChatTargetedBroadcast(message, admin_only) + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/ChatPrivateMessage(message, datum/tgs_chat_user/user) + return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/SecurityLevel() + return TGS_UNIMPLEMENTED diff --git a/code/modules/tgs/core/tgs_version.dm b/code/modules/tgs/core/tgs_version.dm new file mode 100644 index 00000000000..a5dae1241a3 --- /dev/null +++ b/code/modules/tgs/core/tgs_version.dm @@ -0,0 +1,28 @@ +/datum/tgs_version/New(raw_parameter) + src.raw_parameter = raw_parameter + deprefixed_parameter = replacetext(raw_parameter, "/tg/station 13 Server v", "") + var/list/version_bits = splittext(deprefixed_parameter, ".") + + suite = text2num(version_bits[1]) + if(version_bits.len > 1) + minor = text2num(version_bits[2]) + if(version_bits.len > 2) + patch = text2num(version_bits[3]) + if(version_bits.len == 4) + deprecated_patch = text2num(version_bits[4]) + +/datum/tgs_version/proc/Valid(allow_wildcards = FALSE) + if(suite == null) + return FALSE + if(allow_wildcards) + return TRUE + return !Wildcard() + +/datum/tgs_version/Wildcard() + return minor == null || patch == null + +/datum/tgs_version/Equals(datum/tgs_version/other_version) + if(!istype(other_version)) + return FALSE + + return suite == other_version.suite && minor == other_version.minor && patch == other_version.patch && deprecated_patch == other_version.deprecated_patch diff --git a/code/modules/tgs/includes.dm b/code/modules/tgs/includes.dm new file mode 100644 index 00000000000..25e1b8421a8 --- /dev/null +++ b/code/modules/tgs/includes.dm @@ -0,0 +1,18 @@ +#include "core\_definitions.dm" +#include "core\core.dm" +#include "core\datum.dm" +#include "core\tgs_version.dm" + +#ifdef TGS_V3_API +#include "v3210\api.dm" +#include "v3210\commands.dm" +#endif + +#include "v4\api.dm" +#include "v4\commands.dm" + +#include "v5\_defines.dm" +#include "v5\api.dm" +#include "v5\commands.dm" +#include "v5\serializers.dm" +#include "v5\undefs.dm" diff --git a/code/modules/tgs/v3210/README.md b/code/modules/tgs/v3210/README.md new file mode 100644 index 00000000000..f96e7cf3b31 --- /dev/null +++ b/code/modules/tgs/v3210/README.md @@ -0,0 +1,6 @@ +# DMAPI V3 + +This DMAPI implements bridge using file output which TGS monitors for. + +- [api.dm](./api.dm) contains the bulk of the API code. +- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s. diff --git a/code/modules/tgs/v3210/api.dm b/code/modules/tgs/v3210/api.dm new file mode 100644 index 00000000000..b881662d71c --- /dev/null +++ b/code/modules/tgs/v3210/api.dm @@ -0,0 +1,244 @@ +#define REBOOT_MODE_NORMAL 0 +#define REBOOT_MODE_HARD 1 +#define REBOOT_MODE_SHUTDOWN 2 + +#define SERVICE_WORLD_PARAM "server_service" +#define SERVICE_INSTANCE_PARAM "server_instance" +#define SERVICE_PR_TEST_JSON "prtestjob.json" +#define SERVICE_INTERFACE_DLL "TGDreamDaemonBridge.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_LIST_CUSTOM "list_custom_commands" +#define SERVICE_CMD_API_COMPATIBLE "api_compat" +#define SERVICE_CMD_PLAYER_COUNT "client_count" + +#define SERVICE_CMD_PARAM_KEY "serviceCommsKey" +#define SERVICE_CMD_PARAM_COMMAND "command" +#define SERVICE_CMD_PARAM_SENDER "sender" +#define SERVICE_CMD_PARAM_CUSTOM "custom" + +#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" + +#define SERVICE_RETURN_SUCCESS "SUCCESS" + +#define TGS_FILE2LIST(filename) (splittext(trim_left(trim_right(file2text(filename))), "\n")) + +/datum/tgs_api/v3210 + var/reboot_mode = REBOOT_MODE_NORMAL + var/comms_key + var/instance_name + var/originmastercommit + var/commit + var/list/cached_custom_tgs_chat_commands + var/warned_revison = FALSE + var/warned_custom_commands = FALSE + +/datum/tgs_api/v3210/ApiVersion() + return new /datum/tgs_version("3.2.1.3") + +/datum/tgs_api/v3210/proc/trim_left(text) + for (var/i = 1 to length(text)) + if (text2ascii(text, i) > 32) + return copytext(text, i) + return "" + +/datum/tgs_api/v3210/proc/trim_right(text) + for (var/i = length(text), i > 0, i--) + if (text2ascii(text, i) > 32) + return copytext(text, 1, i + 1) + return "" + +/datum/tgs_api/v3210/OnWorldNew(minimum_required_security_level) + . = FALSE + + comms_key = world.params[SERVICE_WORLD_PARAM] + instance_name = world.params[SERVICE_INSTANCE_PARAM] + if(!instance_name) + instance_name = "TG Station Server" //maybe just upgraded + + var/list/logs = TGS_FILE2LIST(".git/logs/HEAD") + if(logs.len) + logs = splittext(logs[logs.len], " ") + if (logs.len >= 2) + commit = logs[2] + else + TGS_ERROR_LOG("Error parsing commit logs") + + logs = TGS_FILE2LIST(".git/logs/refs/remotes/origin/master") + if(logs.len) + logs = splittext(logs[logs.len], " ") + if (logs.len >= 2) + originmastercommit = logs[2] + else + TGS_ERROR_LOG("Error parsing origin commmit logs") + + if(world.system_type != MS_WINDOWS) + TGS_ERROR_LOG("This API version is only supported on Windows. Not running on Windows. Aborting initialization!") + return + ListServiceCustomCommands(TRUE) + var/datum/tgs_version/api_version = ApiVersion() + ExportService("[SERVICE_REQUEST_API_VERSION] [api_version.deprefixed_parameter]", TRUE) + return TRUE + +//nothing to do for v3 +/datum/tgs_api/v3210/OnInitializationComplete() + return + +/datum/tgs_api/v3210/InstanceName() + return world.params[SERVICE_INSTANCE_PARAM] + +/datum/tgs_api/v3210/proc/ExportService(command, skip_compat_check = FALSE) + . = FALSE + if(skip_compat_check && !fexists(SERVICE_INTERFACE_DLL)) + TGS_ERROR_LOG("Service parameter present but no interface DLL detected. This is symptomatic of running a service less than version 3.1! Please upgrade.") + return + #if DM_VERSION >= 515 + call_ext(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval + #else + call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval + #endif + return TRUE + +/datum/tgs_api/v3210/OnTopic(T) + var/list/params = params2list(T) + var/their_sCK = params[SERVICE_CMD_PARAM_KEY] + if(!their_sCK) + return FALSE //continue world/Topic + + if(their_sCK != comms_key) + return "Invalid comms key!"; + + var/command = params[SERVICE_CMD_PARAM_COMMAND] + if(!command) + return "No command!" + + switch(command) + if(SERVICE_CMD_API_COMPATIBLE) + return SERVICE_RETURN_SUCCESS + if(SERVICE_CMD_HARD_REBOOT) + if(reboot_mode != REBOOT_MODE_HARD) + reboot_mode = REBOOT_MODE_HARD + TGS_INFO_LOG("Hard reboot requested by service") + TGS_NOTIFY_ADMINS("The world will hard reboot at the end of the game. Requested by TGS.") + if(SERVICE_CMD_GRACEFUL_SHUTDOWN) + if(reboot_mode != REBOOT_MODE_SHUTDOWN) + reboot_mode = REBOOT_MODE_SHUTDOWN + TGS_INFO_LOG("Shutdown requested by service") + TGS_NOTIFY_ADMINS("The world will shutdown at the end of the game. Requested by TGS.") + if(SERVICE_CMD_WORLD_ANNOUNCE) + var/msg = params["message"] + if(!istext(msg) || !msg) + return "No message set!" + TGS_WORLD_ANNOUNCE(msg) + return SERVICE_RETURN_SUCCESS + if(SERVICE_CMD_PLAYER_COUNT) + return "[TGS_CLIENT_COUNT]" + 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 : SERVICE_RETURN_SUCCESS + return "Unknown command: [command]" + +/datum/tgs_api/v3210/OnReboot() + switch(reboot_mode) + if(REBOOT_MODE_HARD) + TGS_WORLD_ANNOUNCE("Hard reboot triggered, you will automatically reconnect...") + EndProcess() + if(REBOOT_MODE_SHUTDOWN) + TGS_WORLD_ANNOUNCE("The server is shutting down...") + EndProcess() + else + ExportService(SERVICE_REQUEST_WORLD_REBOOT) //just let em know + +/datum/tgs_api/v3210/TestMerges() + //do the best we can here as the datum can't be completed using the v3 api + . = list() + if(!fexists(SERVICE_PR_TEST_JSON)) + return + var/list/json = json_decode(file2text(SERVICE_PR_TEST_JSON)) + if(!json) + return + for(var/I in json) + var/datum/tgs_revision_information/test_merge/tm = new + tm.number = text2num(I) + var/list/entry = json[I] + tm.head_commit = entry["commit"] + tm.author = entry["author"] + tm.title = entry["title"] + . += tm + +/datum/tgs_api/v3210/Revision() + if(!warned_revison) + var/datum/tgs_version/api_version = ApiVersion() + TGS_ERROR_LOG("Use of TgsRevision on [api_version.deprefixed_parameter] origin_commit only points to master!") + warned_revison = TRUE + var/datum/tgs_revision_information/ri = new + ri.commit = commit + ri.origin_commit = originmastercommit + return ri + +/datum/tgs_api/v3210/EndProcess() + sleep(world.tick_lag) //flush the buffers + ExportService(SERVICE_REQUEST_KILL_PROCESS) + +/datum/tgs_api/v3210/ChatChannelInfo() + return list() // :omegalul: + +/datum/tgs_api/v3210/ChatBroadcast(datum/tgs_message_content/message, list/channels) + if(channels) + return TGS_UNIMPLEMENTED + message = UpgradeDeprecatedChatMessage(message) + ChatTargetedBroadcast(message, TRUE) + ChatTargetedBroadcast(message, FALSE) + +/datum/tgs_api/v3210/ChatTargetedBroadcast(datum/tgs_message_content/message, admin_only) + message = UpgradeDeprecatedChatMessage(message) + ExportService("[admin_only ? SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE : SERVICE_REQUEST_IRC_BROADCAST] [message.text]") + +/datum/tgs_api/v3210/ChatPrivateMessage(message, datum/tgs_chat_user/user) + UpgradeDeprecatedChatMessage(message) + return TGS_UNIMPLEMENTED + +/datum/tgs_api/v3210/SecurityLevel() + return TGS_SECURITY_TRUSTED + +#undef REBOOT_MODE_NORMAL +#undef REBOOT_MODE_HARD +#undef REBOOT_MODE_SHUTDOWN + +#undef SERVICE_WORLD_PARAM +#undef SERVICE_INSTANCE_PARAM +#undef SERVICE_PR_TEST_JSON +#undef SERVICE_INTERFACE_DLL +#undef SERVICE_INTERFACE_FUNCTION + +#undef SERVICE_CMD_HARD_REBOOT +#undef SERVICE_CMD_GRACEFUL_SHUTDOWN +#undef SERVICE_CMD_WORLD_ANNOUNCE +#undef SERVICE_CMD_LIST_CUSTOM +#undef SERVICE_CMD_API_COMPATIBLE +#undef SERVICE_CMD_PLAYER_COUNT + +#undef SERVICE_CMD_PARAM_KEY +#undef SERVICE_CMD_PARAM_COMMAND +#undef SERVICE_CMD_PARAM_SENDER +#undef SERVICE_CMD_PARAM_CUSTOM + +#undef SERVICE_REQUEST_KILL_PROCESS +#undef SERVICE_REQUEST_IRC_BROADCAST +#undef SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE +#undef SERVICE_REQUEST_WORLD_REBOOT +#undef SERVICE_REQUEST_API_VERSION + +#undef SERVICE_RETURN_SUCCESS + +#undef TGS_FILE2LIST diff --git a/code/modules/tgs/v3210/commands.dm b/code/modules/tgs/v3210/commands.dm new file mode 100644 index 00000000000..d9bd287465b --- /dev/null +++ b/code/modules/tgs/v3210/commands.dm @@ -0,0 +1,58 @@ +#define SERVICE_JSON_PARAM_HELPTEXT "help_text" +#define SERVICE_JSON_PARAM_ADMINONLY "admin_only" +#define SERVICE_JSON_PARAM_REQUIREDPARAMETERS "required_parameters" + +/datum/tgs_api/v3210/proc/ListServiceCustomCommands(warnings_only) + if(!warnings_only) + . = list() + var/list/command_name_types = list() + var/list/warned_command_names = warnings_only ? list() : null + var/warned_about_the_dangers_of_robutussin = !warnings_only + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) + if(!warned_about_the_dangers_of_robutussin) + TGS_WARNING_LOG("Custom chat commands in [ApiVersion()] lacks the /datum/tgs_chat_user/sender.channel field!") + warned_about_the_dangers_of_robutussin = TRUE + var/datum/tgs_chat_command/stc = I + if(stc.ignore_type == I) + continue + + var/command_name = initial(stc.name) + if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\"")) + if(warnings_only && !warned_command_names[command_name]) + TGS_ERROR_LOG("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) + TGS_ERROR_LOG("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 = 0) + +/datum/tgs_api/v3210/proc/HandleServiceCustomCommand(command, sender, params) + if(!cached_custom_tgs_chat_commands) + cached_custom_tgs_chat_commands = list() + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) + var/datum/tgs_chat_command/stc = I + cached_custom_tgs_chat_commands[lowertext(initial(stc.name))] = stc + + var/command_type = cached_custom_tgs_chat_commands[command] + if(!command_type) + return FALSE + var/datum/tgs_chat_command/stc = new command_type + var/datum/tgs_chat_user/user = new + user.friendly_name = sender + + // Discord hack, fix the mention if it's only numbers (fuck you IRC trolls) + var/regex/discord_id_regex = regex(@"^[0-9]+$") + if(findtext(sender, discord_id_regex)) + sender = "<@[sender]>" + + user.mention = sender + var/datum/tgs_message_content/result = stc.Run(user, params) + result = UpgradeDeprecatedCommandResponse(result, command) + + return result?.text || TRUE diff --git a/code/modules/tgs/v4/README.md b/code/modules/tgs/v4/README.md new file mode 100644 index 00000000000..78191447b27 --- /dev/null +++ b/code/modules/tgs/v4/README.md @@ -0,0 +1,6 @@ +# DMAPI V4 + +This DMAPI implements bridge requests using file output which TGS monitors for. It has a safe mode restriction. + +- [api.dm](./api.dm) contains the bulk of the API code. +- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s. diff --git a/code/modules/tgs/v4/api.dm b/code/modules/tgs/v4/api.dm new file mode 100644 index 00000000000..2f05c386338 --- /dev/null +++ b/code/modules/tgs/v4/api.dm @@ -0,0 +1,312 @@ +#define TGS4_PARAM_INFO_JSON "tgs_json" + +#define TGS4_INTEROP_ACCESS_IDENTIFIER "tgs_tok" + +#define TGS4_RESPONSE_SUCCESS "tgs_succ" + +#define TGS4_TOPIC_CHANGE_PORT "tgs_port" +#define TGS4_TOPIC_CHANGE_REBOOT_MODE "tgs_rmode" +#define TGS4_TOPIC_CHAT_COMMAND "tgs_chat_comm" +#define TGS4_TOPIC_EVENT "tgs_event" +#define TGS4_TOPIC_INTEROP_RESPONSE "tgs_interop" + +#define TGS4_COMM_NEW_PORT "tgs_new_port" +#define TGS4_COMM_VALIDATE "tgs_validate" +#define TGS4_COMM_SERVER_PRIMED "tgs_prime" +#define TGS4_COMM_WORLD_REBOOT "tgs_reboot" +#define TGS4_COMM_END_PROCESS "tgs_kill" +#define TGS4_COMM_CHAT "tgs_chat_send" + +#define TGS4_PARAMETER_COMMAND "tgs_com" +#define TGS4_PARAMETER_DATA "tgs_data" + +#define TGS4_PORT_CRITFAIL_MESSAGE " Must exit to let watchdog reboot..." + +#define EXPORT_TIMEOUT_DS 200 + +/datum/tgs_api/v4 + var/access_identifier + var/instance_name + var/json_path + var/chat_channels_json_path + var/chat_commands_json_path + var/server_commands_json_path + var/reboot_mode = TGS_REBOOT_MODE_NORMAL + var/security_level + + var/requesting_new_port = FALSE + + var/list/intercepted_message_queue + + var/list/custom_commands + + var/list/cached_test_merges + var/datum/tgs_revision_information/cached_revision + + var/export_lock = FALSE + var/list/last_interop_response + +/datum/tgs_api/v4/ApiVersion() + return new /datum/tgs_version("4.0.0.0") + +/datum/tgs_api/v4/OnWorldNew(minimum_required_security_level) + if(minimum_required_security_level == TGS_SECURITY_ULTRASAFE) + TGS_WARNING_LOG("V4 DMAPI requires safe security!") + minimum_required_security_level = TGS_SECURITY_SAFE + + json_path = world.params[TGS4_PARAM_INFO_JSON] + if(!json_path) + TGS_ERROR_LOG("Missing [TGS4_PARAM_INFO_JSON] world parameter!") + return + var/json_file = file2text(json_path) + if(!json_file) + TGS_ERROR_LOG("Missing specified json file: [json_path]") + return + var/cached_json = json_decode(json_file) + if(!cached_json) + TGS_ERROR_LOG("Failed to decode info json: [json_file]") + return + + access_identifier = cached_json["accessIdentifier"] + server_commands_json_path = cached_json["serverCommandsJson"] + + if(cached_json["apiValidateOnly"]) + TGS_INFO_LOG("Validating API and exiting...") + Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]")) + del(world) + + security_level = cached_json["securityLevel"] + chat_channels_json_path = cached_json["chatChannelsJson"] + chat_commands_json_path = cached_json["chatCommandsJson"] + instance_name = cached_json["instanceName"] + + ListCustomCommands() + + var/list/revisionData = cached_json["revision"] + if(revisionData) + cached_revision = new + cached_revision.commit = revisionData["commitSha"] + cached_revision.origin_commit = revisionData["originCommitSha"] + + cached_test_merges = list() + var/list/json = cached_json["testMerges"] + for(var/entry in json) + var/datum/tgs_revision_information/test_merge/tm = new + tm.timestamp = text2num(entry["timeMerged"]) + + var/list/revInfo = entry["revision"] + if(revInfo) + tm.commit = revInfo["commitSha"] + tm.origin_commit = revInfo["originCommitSha"] + + tm.title = entry["titleAtMerge"] + tm.body = entry["bodyAtMerge"] + tm.url = entry["url"] + tm.author = entry["author"] + tm.number = entry["number"] + tm.head_commit = entry["pullRequestRevision"] + tm.comment = entry["comment"] + + cached_test_merges += tm + + return TRUE + +/datum/tgs_api/v4/OnInitializationComplete() + Export(TGS4_COMM_SERVER_PRIMED) + +/datum/tgs_api/v4/OnTopic(T) + var/list/params = params2list(T) + var/their_sCK = params[TGS4_INTEROP_ACCESS_IDENTIFIER] + if(!their_sCK) + return FALSE //continue world/Topic + + if(their_sCK != access_identifier) + return "Invalid comms key!"; + + var/command = params[TGS4_PARAMETER_COMMAND] + if(!command) + return "No command!" + + . = TGS4_RESPONSE_SUCCESS + + switch(command) + if(TGS4_TOPIC_CHAT_COMMAND) + var/result = HandleCustomCommand(params[TGS4_PARAMETER_DATA]) + if(result == null) + result = "Error running chat command!" + return result + if(TGS4_TOPIC_EVENT) + intercepted_message_queue = list() + var/list/event_notification = json_decode(params[TGS4_PARAMETER_DATA]) + var/list/event_parameters = event_notification["Parameters"] + + var/list/event_call = list(event_notification["Type"]) + if(event_parameters) + event_call += event_parameters + + if(event_handler != null) + event_handler.HandleEvent(arglist(event_call)) + + . = json_encode(intercepted_message_queue) + intercepted_message_queue = null + return + if(TGS4_TOPIC_INTEROP_RESPONSE) + last_interop_response = json_decode(params[TGS4_PARAMETER_DATA]) + return + if(TGS4_TOPIC_CHANGE_PORT) + var/new_port = text2num(params[TGS4_PARAMETER_DATA]) + if (!(new_port > 0)) + return "Invalid port: [new_port]" + + //the topic still completes, miraculously + //I honestly didn't believe byond could do it + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) + if(!world.OpenPort(new_port)) + return "Port change failed!" + return + if(TGS4_TOPIC_CHANGE_REBOOT_MODE) + var/new_reboot_mode = text2num(params[TGS4_PARAMETER_DATA]) + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) + reboot_mode = new_reboot_mode + return + + return "Unknown command: [command]" + +/datum/tgs_api/v4/proc/Export(command, list/data, override_requesting_new_port = FALSE) + if(!data) + data = list() + data[TGS4_PARAMETER_COMMAND] = command + var/json = json_encode(data) + + while(requesting_new_port && !override_requesting_new_port) + sleep(1) + + //we need some port open at this point to facilitate return communication + if(!world.port) + requesting_new_port = TRUE + if(!world.OpenPort(0)) //open any port + TGS_ERROR_LOG("Unable to open random port to retrieve new port![TGS4_PORT_CRITFAIL_MESSAGE]") + del(world) + + //request a new port + export_lock = FALSE + var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose + + if(!new_port_json) + TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]") + del(world) + + var/new_port = new_port_json[TGS4_PARAMETER_DATA] + if(!isnum(new_port) || new_port <= 0) + TGS_ERROR_LOG("Malformed new port json ([json_encode(new_port_json)])![TGS4_PORT_CRITFAIL_MESSAGE]") + del(world) + + if(new_port != world.port && !world.OpenPort(new_port)) + TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]") + del(world) + requesting_new_port = FALSE + + while(export_lock) + sleep(1) + export_lock = TRUE + + last_interop_response = null + fdel(server_commands_json_path) + text2file(json, server_commands_json_path) + + for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I) + sleep(1) + + if(!last_interop_response) + TGS_ERROR_LOG("Failed to get export result for: [json]") + else + . = last_interop_response + + export_lock = FALSE + +/datum/tgs_api/v4/OnReboot() + var/list/result = Export(TGS4_COMM_WORLD_REBOOT) + if(!result) + return + + //okay so the standard TGS4 proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter + + var/port = result[TGS4_PARAMETER_DATA] + if(!isnum(port)) + return //this is valid, server may just want use to reboot + + if(port == 0) + //to byond 0 means any port and "none" means close vOv + port = "none" + + if(!world.OpenPort(port)) + TGS_ERROR_LOG("Unable to set port to [port]!") + +/datum/tgs_api/v4/InstanceName() + return instance_name + +/datum/tgs_api/v4/TestMerges() + return cached_test_merges.Copy() + +/datum/tgs_api/v4/EndProcess() + Export(TGS4_COMM_END_PROCESS) + +/datum/tgs_api/v4/Revision() + return cached_revision + +/datum/tgs_api/v4/ChatBroadcast(datum/tgs_message_content/message, list/channels) + var/list/ids + if(length(channels)) + ids = list() + for(var/I in channels) + var/datum/tgs_chat_channel/channel = I + ids += channel.id + message = UpgradeDeprecatedChatMessage(message) + message = list("message" = message.text, "channelIds" = ids) + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Export(TGS4_COMM_CHAT, message) + +/datum/tgs_api/v4/ChatTargetedBroadcast(datum/tgs_message_content/message, admin_only) + var/list/channels = list() + for(var/I in ChatChannelInfo()) + var/datum/tgs_chat_channel/channel = I + if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only))) + channels += channel.id + message = UpgradeDeprecatedChatMessage(message) + message = list("message" = message.text, "channelIds" = channels) + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Export(TGS4_COMM_CHAT, message) + +/datum/tgs_api/v4/ChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user) + message = UpgradeDeprecatedChatMessage(message) + message = list("message" = message.text, "channelIds" = list(user.channel.id)) + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Export(TGS4_COMM_CHAT, message) + +/datum/tgs_api/v4/ChatChannelInfo() + . = list() + //no caching cause tgs may change this + var/list/json = json_decode(file2text(chat_channels_json_path)) + for(var/I in json) + . += DecodeChannel(I) + +/datum/tgs_api/v4/proc/DecodeChannel(channel_json) + var/datum/tgs_chat_channel/channel = new + channel.id = channel_json["id"] + channel.friendly_name = channel_json["friendlyName"] + channel.connection_name = channel_json["connectionName"] + channel.is_admin_channel = channel_json["isAdminChannel"] + channel.is_private_channel = channel_json["isPrivateChannel"] + channel.custom_tag = channel_json["tag"] + return channel + +/datum/tgs_api/v4/SecurityLevel() + return security_level diff --git a/code/modules/tgs/v4/commands.dm b/code/modules/tgs/v4/commands.dm new file mode 100644 index 00000000000..d6d3d718d47 --- /dev/null +++ b/code/modules/tgs/v4/commands.dm @@ -0,0 +1,44 @@ +/datum/tgs_api/v4/proc/ListCustomCommands() + var/results = list() + custom_commands = list() + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) + var/datum/tgs_chat_command/stc = new I + if(stc.ignore_type == I) + continue + + var/command_name = stc.name + if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\"")) + TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!") + continue + + if(results[command_name]) + var/datum/other = custom_commands[command_name] + TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!") + continue + results += list(list("name" = command_name, "help_text" = stc.help_text, "admin_only" = stc.admin_only)) + custom_commands[command_name] = stc + + var/commands_file = chat_commands_json_path + if(!commands_file) + return + text2file(json_encode(results), commands_file) + +/datum/tgs_api/v4/proc/HandleCustomCommand(command_json) + var/list/data = json_decode(command_json) + var/command = data["command"] + var/user = data["user"] + var/params = data["params"] + + var/datum/tgs_chat_user/u = new + u.id = user["id"] + u.friendly_name = user["friendlyName"] + u.mention = user["mention"] + u.channel = DecodeChannel(user["channel"]) + + var/datum/tgs_chat_command/sc = custom_commands[command] + if(sc) + var/datum/tgs_message_content/result = sc.Run(u, params) + result = UpgradeDeprecatedCommandResponse(result, command) + + return result?.text + return "Unknown command: [command]!" diff --git a/code/modules/tgs/v5/README.md b/code/modules/tgs/v5/README.md new file mode 100644 index 00000000000..619b58cd724 --- /dev/null +++ b/code/modules/tgs/v5/README.md @@ -0,0 +1,10 @@ +# DMAPI V5 + +This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no security restrictions. + +- [__interop_version.dm](./__interop_version.dm) contains the version of the API used between the DMAPI and TGS. +- [_defines.dm](./_defines.dm) contains constant definitions. +- [api.dm](./api.dm) contains the bulk of the API code. +- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s. +- [serializers.dm](./serializers.dm) contains function to help convert interop `/datum`s into a JSON encodable `list()` format. +- [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`. diff --git a/code/modules/tgs/v5/__interop_version.dm b/code/modules/tgs/v5/__interop_version.dm new file mode 100644 index 00000000000..4add7374ad7 --- /dev/null +++ b/code/modules/tgs/v5/__interop_version.dm @@ -0,0 +1 @@ +"5.4.0" diff --git a/code/modules/tgs/v5/_defines.dm b/code/modules/tgs/v5/_defines.dm new file mode 100644 index 00000000000..7f31c23ef4f --- /dev/null +++ b/code/modules/tgs/v5/_defines.dm @@ -0,0 +1,100 @@ +#define DMAPI5_PARAM_SERVER_PORT "tgs_port" +#define DMAPI5_PARAM_ACCESS_IDENTIFIER "tgs_key" + +#define DMAPI5_BRIDGE_DATA "data" +#define DMAPI5_TOPIC_DATA "tgs_data" + +#define DMAPI5_BRIDGE_COMMAND_PORT_UPDATE 0 +#define DMAPI5_BRIDGE_COMMAND_STARTUP 1 +#define DMAPI5_BRIDGE_COMMAND_PRIME 2 +#define DMAPI5_BRIDGE_COMMAND_REBOOT 3 +#define DMAPI5_BRIDGE_COMMAND_KILL 4 +#define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5 + +#define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" +#define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" + +#define DMAPI5_RESPONSE_ERROR_MESSAGE "errorMessage" + +#define DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE "commandType" +#define DMAPI5_BRIDGE_PARAMETER_CURRENT_PORT "currentPort" +#define DMAPI5_BRIDGE_PARAMETER_VERSION "version" +#define DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE "chatMessage" +#define DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel" + +#define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort" +#define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation" + +#define DMAPI5_CHAT_MESSAGE_CHANNEL_IDS "channelIds" + +#define DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER "accessIdentifier" +#define DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION "serverVersion" +#define DMAPI5_RUNTIME_INFORMATION_SERVER_PORT "serverPort" +#define DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY "apiValidateOnly" +#define DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME "instanceName" +#define DMAPI5_RUNTIME_INFORMATION_REVISION "revision" +#define DMAPI5_RUNTIME_INFORMATION_TEST_MERGES "testMerges" +#define DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL "securityLevel" + +#define DMAPI5_CHAT_UPDATE_CHANNELS "channels" + +#define DMAPI5_TEST_MERGE_TIME_MERGED "timeMerged" +#define DMAPI5_TEST_MERGE_REVISION "revision" +#define DMAPI5_TEST_MERGE_TITLE_AT_MERGE "titleAtMerge" +#define DMAPI5_TEST_MERGE_BODY_AT_MERGE "bodyAtMerge" +#define DMAPI5_TEST_MERGE_URL "url" +#define DMAPI5_TEST_MERGE_AUTHOR "author" +#define DMAPI5_TEST_MERGE_NUMBER "number" +#define DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION "pullRequestRevision" +#define DMAPI5_TEST_MERGE_COMMENT "comment" + +#define DMAPI5_CHAT_COMMAND_NAME "name" +#define DMAPI5_CHAT_COMMAND_PARAMS "params" +#define DMAPI5_CHAT_COMMAND_USER "user" + +#define DMAPI5_EVENT_NOTIFICATION_TYPE "type" +#define DMAPI5_EVENT_NOTIFICATION_PARAMETERS "parameters" + +#define DMAPI5_TOPIC_COMMAND_CHAT_COMMAND 0 +#define DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION 1 +#define DMAPI5_TOPIC_COMMAND_CHANGE_PORT 2 +#define DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE 3 +#define DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED 4 +#define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 5 +#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6 +#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7 +#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8 + +#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType" +#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand" +#define DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION "eventNotification" +#define DMAPI5_TOPIC_PARAMETER_NEW_PORT "newPort" +#define DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE "newRebootState" +#define DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME "newInstanceName" +#define DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE "chatUpdate" +#define DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION "newServerVersion" + +#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE "commandResponse" +#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE "commandResponseMessage" +#define DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES "chatResponses" + +#define DMAPI5_REVISION_INFORMATION_COMMIT_SHA "commitSha" +#define DMAPI5_REVISION_INFORMATION_TIMESTAMP "timestamp" +#define DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA "originCommitSha" + +#define DMAPI5_CHAT_USER_ID "id" +#define DMAPI5_CHAT_USER_FRIENDLY_NAME "friendlyName" +#define DMAPI5_CHAT_USER_MENTION "mention" +#define DMAPI5_CHAT_USER_CHANNEL "channel" + +#define DMAPI5_CHAT_CHANNEL_ID "id" +#define DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME "friendlyName" +#define DMAPI5_CHAT_CHANNEL_CONNECTION_NAME "connectionName" +#define DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL "isAdminChannel" +#define DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL "isPrivateChannel" +#define DMAPI5_CHAT_CHANNEL_TAG "tag" +#define DMAPI5_CHAT_CHANNEL_EMBEDS_SUPPORTED "embedsSupported" + +#define DMAPI5_CUSTOM_CHAT_COMMAND_NAME "name" +#define DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT "helpText" +#define DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY "adminOnly" diff --git a/code/modules/tgs/v5/api.dm b/code/modules/tgs/v5/api.dm new file mode 100644 index 00000000000..36610b1242a --- /dev/null +++ b/code/modules/tgs/v5/api.dm @@ -0,0 +1,378 @@ +/datum/tgs_api/v5 + var/server_port + var/access_identifier + + var/instance_name + var/security_level + + var/reboot_mode = TGS_REBOOT_MODE_NORMAL + + var/list/intercepted_message_queue + + var/list/custom_commands + + var/list/test_merges + var/datum/tgs_revision_information/revision + var/list/chat_channels + + var/initialized = FALSE + +/datum/tgs_api/v5/ApiVersion() + return new /datum/tgs_version( + #include "__interop_version.dm" + ) + +/datum/tgs_api/v5/OnWorldNew(minimum_required_security_level) + server_port = world.params[DMAPI5_PARAM_SERVER_PORT] + access_identifier = world.params[DMAPI5_PARAM_ACCESS_IDENTIFIER] + + var/datum/tgs_version/api_version = ApiVersion() + version = null + var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) + if(!istype(bridge_response)) + TGS_ERROR_LOG("Failed initial bridge request!") + return FALSE + + var/list/runtime_information = bridge_response[DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION] + if(!istype(runtime_information)) + TGS_ERROR_LOG("Failed to decode runtime information from bridge response: [json_encode(bridge_response)]!") + return FALSE + + if(runtime_information[DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY]) + TGS_INFO_LOG("DMAPI validation, exiting...") + del(world) + + version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) + security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL] + instance_name = runtime_information[DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME] + + var/list/revisionData = runtime_information[DMAPI5_RUNTIME_INFORMATION_REVISION] + if(istype(revisionData)) + revision = new + revision.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA] + revision.timestamp = revisionData[DMAPI5_REVISION_INFORMATION_TIMESTAMP] + revision.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA] + else + TGS_ERROR_LOG("Failed to decode [DMAPI5_RUNTIME_INFORMATION_REVISION] from runtime information!") + + test_merges = list() + var/list/test_merge_json = runtime_information[DMAPI5_RUNTIME_INFORMATION_TEST_MERGES] + if(istype(test_merge_json)) + for(var/entry in test_merge_json) + var/datum/tgs_revision_information/test_merge/tm = new + tm.number = entry[DMAPI5_TEST_MERGE_NUMBER] + + var/list/revInfo = entry[DMAPI5_TEST_MERGE_REVISION] + if(revInfo) + tm.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA] + tm.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA] + tm.timestamp = entry[DMAPI5_REVISION_INFORMATION_TIMESTAMP] + else + TGS_WARNING_LOG("Failed to decode [DMAPI5_TEST_MERGE_REVISION] from test merge #[tm.number]!") + + if(!tm.timestamp) + tm.timestamp = entry[DMAPI5_TEST_MERGE_TIME_MERGED] + + tm.title = entry[DMAPI5_TEST_MERGE_TITLE_AT_MERGE] + tm.body = entry[DMAPI5_TEST_MERGE_BODY_AT_MERGE] + tm.url = entry[DMAPI5_TEST_MERGE_URL] + tm.author = entry[DMAPI5_TEST_MERGE_AUTHOR] + tm.head_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION] + tm.comment = entry[DMAPI5_TEST_MERGE_COMMENT] + + test_merges += tm + else + TGS_WARNING_LOG("Failed to decode [DMAPI5_RUNTIME_INFORMATION_TEST_MERGES] from runtime information!") + + chat_channels = list() + DecodeChannels(runtime_information) + + initialized = TRUE + return TRUE + +/datum/tgs_api/v5/proc/RequireInitialBridgeResponse() + while(!version) + sleep(1) + +/datum/tgs_api/v5/OnInitializationComplete() + Bridge(DMAPI5_BRIDGE_COMMAND_PRIME) + +/datum/tgs_api/v5/proc/TopicResponse(error_message = null) + var/list/response = list() + response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message + + return json_encode(response) + +/datum/tgs_api/v5/OnTopic(T) + RequireInitialBridgeResponse() + var/list/params = params2list(T) + var/json = params[DMAPI5_TOPIC_DATA] + if(!json) + return FALSE // continue to /world/Topic + + var/list/topic_parameters = json_decode(json) + if(!topic_parameters) + return TopicResponse("Invalid topic parameters json!"); + + if(!initialized) + TGS_WARNING_LOG("Missed topic due to not being initialized: [T]") + return TRUE // too early to handle, but it's still our responsibility + + var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] + if(their_sCK != access_identifier) + return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!"); + + var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] + if(!isnum(command)) + return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] from: [json]!") + + switch(command) + if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND) + var/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) + if(!result) + result = TopicResponse("Error running chat command!") + return result + if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION) + intercepted_message_queue = list() + var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION] + if(!istype(event_notification)) + return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!") + + var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE] + if(!isnum(event_type)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!") + + var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS] + if(event_parameters && !istype(event_parameters)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!") + + var/list/event_call = list(event_type) + if(event_parameters) + event_call += event_parameters + + if(event_handler != null) + event_handler.HandleEvent(arglist(event_call)) + + var/list/response = list() + response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue + intercepted_message_queue = null + return json_encode(response) + if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + if (!isnum(new_port) || !(new_port > 0)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) + + //the topic still completes, miraculously + //I honestly didn't believe byond could do it without exploding + if(!world.OpenPort(new_port)) + return TopicResponse("Port change failed!") + + return TopicResponse() + if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE) + var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE] + if(!isnum(new_reboot_mode)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) + + reboot_mode = new_reboot_mode + return TopicResponse() + if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED) + var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME] + if(!istext(new_instance_name)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name) + + instance_name = new_instance_name + return TopicResponse() + if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE) + var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE] + if(!istype(chat_update_json)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!") + + DecodeChannels(chat_update_json) + return TopicResponse() + if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + if (!isnum(new_port) || !(new_port > 0)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + + server_port = new_port + return TopicResponse() + if(DMAPI5_TOPIC_COMMAND_HEARTBEAT) + return TopicResponse() + if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + var/error_message = null + if (new_port != null) + if (!isnum(new_port) || !(new_port > 0)) + error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]" + else + server_port = new_port + + var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION] + if (!istext(new_version_string)) + if(error_message != null) + error_message += ", " + error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]" + else + var/datum/tgs_version/new_version = new(new_version_string) + if (event_handler) + event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version) + + version = new_version + + return json_encode(list(DMAPI5_RESPONSE_ERROR_MESSAGE = error_message, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) + + return TopicResponse("Unknown command: [command]") + +/datum/tgs_api/v5/proc/Bridge(command, list/data) + if(!data) + data = list() + + data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command + data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier + + var/json = json_encode(data) + var/encoded_json = url_encode(json) + + // This is an infinite sleep until we get a response + var/export_response = world.Export("http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]") + if(!export_response) + TGS_ERROR_LOG("Failed export request: [json]") + return + + var/response_json = file2text(export_response["CONTENT"]) + if(!response_json) + TGS_ERROR_LOG("Failed export request, missing content!") + return + + var/list/bridge_response = json_decode(response_json) + if(!bridge_response) + TGS_ERROR_LOG("Failed export request, bad json: [response_json]") + return + + var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE] + if(error) + TGS_ERROR_LOG("Failed export request, bad request: [error]") + return + + return bridge_response + +/datum/tgs_api/v5/OnReboot() + var/list/result = Bridge(DMAPI5_BRIDGE_COMMAND_REBOOT) + if(!result) + return + + //okay so the standard TGS proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter + + var/port = result[DMAPI5_BRIDGE_RESPONSE_NEW_PORT] + if(!isnum(port)) + return //this is valid, server may just want use to reboot + + if(port == 0) + //to byond 0 means any port and "none" means close vOv + port = "none" + + if(!world.OpenPort(port)) + TGS_ERROR_LOG("Unable to set port to [port]!") + +/datum/tgs_api/v5/InstanceName() + RequireInitialBridgeResponse() + return instance_name + +/datum/tgs_api/v5/TestMerges() + RequireInitialBridgeResponse() + return test_merges.Copy() + +/datum/tgs_api/v5/EndProcess() + Bridge(DMAPI5_BRIDGE_COMMAND_KILL) + +/datum/tgs_api/v5/Revision() + RequireInitialBridgeResponse() + return revision + +// Common proc b/c it's used by the V3/V4 APIs +/datum/tgs_api/proc/UpgradeDeprecatedChatMessage(datum/tgs_message_content/message) + if(!istext(message)) + return message + + TGS_WARNING_LOG("Received legacy string when a [/datum/tgs_message_content] was expected. Please audit all calls to TgsChatBroadcast, TgsChatTargetedBroadcast, and TgsChatPrivateMessage to ensure they use the new /datum.") + return new /datum/tgs_message_content(message) + +/datum/tgs_api/v5/ChatBroadcast(datum/tgs_message_content/message, list/channels) + if(!length(channels)) + channels = ChatChannelInfo() + + var/list/ids = list() + for(var/I in channels) + var/datum/tgs_chat_channel/channel = I + ids += channel.id + + message = UpgradeDeprecatedChatMessage(message) + message = message._interop_serialize() + message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = ids + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message)) + +/datum/tgs_api/v5/ChatTargetedBroadcast(datum/tgs_message_content/message, admin_only) + var/list/channels = list() + for(var/I in ChatChannelInfo()) + var/datum/tgs_chat_channel/channel = I + if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only))) + channels += channel.id + + message = UpgradeDeprecatedChatMessage(message) + message = message._interop_serialize() + message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = channels + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message)) + +/datum/tgs_api/v5/ChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user) + message = UpgradeDeprecatedChatMessage(message) + message = message._interop_serialize() + message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = list(user.channel.id) + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message)) + +/datum/tgs_api/v5/ChatChannelInfo() + RequireInitialBridgeResponse() + return chat_channels.Copy() + +/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json) + var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS] + if(istype(chat_channels_json)) + chat_channels.Cut() + for(var/channel_json in chat_channels_json) + var/datum/tgs_chat_channel/channel = DecodeChannel(channel_json) + if(channel) + chat_channels += channel + else + TGS_WARNING_LOG("Failed to decode [DMAPI5_CHAT_UPDATE_CHANNELS] from channel update!") + +/datum/tgs_api/v5/proc/DecodeChannel(channel_json) + var/datum/tgs_chat_channel/channel = new + channel.id = channel_json[DMAPI5_CHAT_CHANNEL_ID] + channel.friendly_name = channel_json[DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME] + channel.connection_name = channel_json[DMAPI5_CHAT_CHANNEL_CONNECTION_NAME] + channel.is_admin_channel = channel_json[DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL] + channel.is_private_channel = channel_json[DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL] + channel.custom_tag = channel_json[DMAPI5_CHAT_CHANNEL_TAG] + channel.embeds_supported = channel_json[DMAPI5_CHAT_CHANNEL_EMBEDS_SUPPORTED] + return channel + +/datum/tgs_api/v5/SecurityLevel() + RequireInitialBridgeResponse() + return security_level diff --git a/code/modules/tgs/v5/commands.dm b/code/modules/tgs/v5/commands.dm new file mode 100644 index 00000000000..71ede42c3b2 --- /dev/null +++ b/code/modules/tgs/v5/commands.dm @@ -0,0 +1,60 @@ +/datum/tgs_api/v5/proc/ListCustomCommands() + var/results = list() + custom_commands = list() + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) + var/datum/tgs_chat_command/stc = new I + if(stc.ignore_type == I) + continue + + var/command_name = stc.name + if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\"")) + TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!") + continue + + if(results[command_name]) + var/datum/other = custom_commands[command_name] + TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!") + continue + results += list(list(DMAPI5_CUSTOM_CHAT_COMMAND_NAME = command_name, DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT = stc.help_text, DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY = stc.admin_only)) + custom_commands[command_name] = stc + + return results + +/datum/tgs_api/v5/proc/HandleCustomCommand(list/command_json) + var/command = command_json[DMAPI5_CHAT_COMMAND_NAME] + var/user = command_json[DMAPI5_CHAT_COMMAND_USER] + var/params = command_json[DMAPI5_CHAT_COMMAND_PARAMS] + + var/datum/tgs_chat_user/u = new + u.id = user[DMAPI5_CHAT_USER_ID] + u.friendly_name = user[DMAPI5_CHAT_USER_FRIENDLY_NAME] + u.mention = user[DMAPI5_CHAT_USER_MENTION] + u.channel = DecodeChannel(user[DMAPI5_CHAT_USER_CHANNEL]) + + var/datum/tgs_chat_command/sc = custom_commands[command] + if(sc) + var/datum/tgs_message_content/response = sc.Run(u, params) + response = UpgradeDeprecatedCommandResponse(response, command) + + var/list/topic_response = list() + topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = response?.text + topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE] = response?._interop_serialize() + return json_encode(topic_response) + return TopicResponse("Unknown custom chat command: [command]!") + +// Common proc b/c it's used by the V3/V4 APIs +/datum/tgs_api/proc/UpgradeDeprecatedCommandResponse(datum/tgs_message_content/response, command) + // Backwards compatibility, used to return a string + if(istext(response)) + warned_deprecated_command_runs = warned_deprecated_command_runs || list() + if(!warned_deprecated_command_runs[command]) + TGS_WARNING_LOG("Custom chat command \"[command]\" is still returning a string. This behaviour is deprecated, please upgrade it to return a [/datum/tgs_message_content].") + warned_deprecated_command_runs[command] = TRUE + + return new /datum/tgs_message_content(response) + + if(!istype(response)) + TGS_ERROR_LOG("Custom chat command \"[command]\" should return a [/datum/tgs_message_content]! Got: \"[response]\"") + return null + + return response diff --git a/code/modules/tgs/v5/serializers.dm b/code/modules/tgs/v5/serializers.dm new file mode 100644 index 00000000000..38814e2d9f5 --- /dev/null +++ b/code/modules/tgs/v5/serializers.dm @@ -0,0 +1,52 @@ +/datum/tgs_message_content/proc/_interop_serialize() + return list("text" = text, "embed" = embed?._interop_serialize()) + +/datum/tgs_chat_embed/proc/_interop_serialize() + CRASH("Base /proc/interop_serialize called on [type]!") + +/datum/tgs_chat_embed/structure/_interop_serialize() + var/list/serialized_fields + if(islist(fields)) + serialized_fields = list() + for(var/datum/tgs_chat_embed/field/field as anything in fields) + serialized_fields += list(field._interop_serialize()) + return list( + "title" = title, + "description" = description, + "url" = url, + "timestamp" = timestamp, + "colour" = colour, + "image" = image?._interop_serialize(), + "thumbnail" = thumbnail?._interop_serialize(), + "video" = video?._interop_serialize(), + "footer" = footer?._interop_serialize(), + "provider" = provider?._interop_serialize(), + "author" = author?._interop_serialize(), + "fields" = serialized_fields + ) + +/datum/tgs_chat_embed/media/_interop_serialize() + return list( + "url" = url, + "width" = width, + "height" = height, + "proxyUrl" = proxy_url + ) + +/datum/tgs_chat_embed/provider/_interop_serialize() + return list( + "url" = url, + "name" = name + ) + +/datum/tgs_chat_embed/provider/author/_interop_serialize() + . = ..() + .["iconUrl"] = icon_url + .["proxyIconUrl"] = proxy_icon_url + +/datum/tgs_chat_embed/field/_interop_serialize() + return list( + "name" = name, + "value" = value, + "isInline" = is_inline + ) diff --git a/code/modules/tgs/v5/undefs.dm b/code/modules/tgs/v5/undefs.dm new file mode 100644 index 00000000000..62099453724 --- /dev/null +++ b/code/modules/tgs/v5/undefs.dm @@ -0,0 +1,100 @@ +#undef DMAPI5_PARAM_SERVER_PORT +#undef DMAPI5_PARAM_ACCESS_IDENTIFIER + +#undef DMAPI5_BRIDGE_DATA +#undef DMAPI5_TOPIC_DATA + +#undef DMAPI5_BRIDGE_COMMAND_PORT_UPDATE +#undef DMAPI5_BRIDGE_COMMAND_STARTUP +#undef DMAPI5_BRIDGE_COMMAND_PRIME +#undef DMAPI5_BRIDGE_COMMAND_REBOOT +#undef DMAPI5_BRIDGE_COMMAND_KILL +#undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND + +#undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER +#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS + +#undef DMAPI5_RESPONSE_ERROR_MESSAGE + +#undef DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE +#undef DMAPI5_BRIDGE_PARAMETER_CURRENT_PORT +#undef DMAPI5_BRIDGE_PARAMETER_VERSION +#undef DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE +#undef DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL + +#undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT +#undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION + +#undef DMAPI5_CHAT_MESSAGE_CHANNEL_IDS + +#undef DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER +#undef DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION +#undef DMAPI5_RUNTIME_INFORMATION_SERVER_PORT +#undef DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY +#undef DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME +#undef DMAPI5_RUNTIME_INFORMATION_REVISION +#undef DMAPI5_RUNTIME_INFORMATION_TEST_MERGES +#undef DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL + +#undef DMAPI5_CHAT_UPDATE_CHANNELS + +#undef DMAPI5_TEST_MERGE_TIME_MERGED +#undef DMAPI5_TEST_MERGE_REVISION +#undef DMAPI5_TEST_MERGE_TITLE_AT_MERGE +#undef DMAPI5_TEST_MERGE_BODY_AT_MERGE +#undef DMAPI5_TEST_MERGE_URL +#undef DMAPI5_TEST_MERGE_AUTHOR +#undef DMAPI5_TEST_MERGE_NUMBER +#undef DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION +#undef DMAPI5_TEST_MERGE_COMMENT + +#undef DMAPI5_CHAT_COMMAND_NAME +#undef DMAPI5_CHAT_COMMAND_PARAMS +#undef DMAPI5_CHAT_COMMAND_USER + +#undef DMAPI5_EVENT_NOTIFICATION_TYPE +#undef DMAPI5_EVENT_NOTIFICATION_PARAMETERS + +#undef DMAPI5_TOPIC_COMMAND_CHAT_COMMAND +#undef DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION +#undef DMAPI5_TOPIC_COMMAND_CHANGE_PORT +#undef DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE +#undef DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED +#undef DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE +#undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE +#undef DMAPI5_TOPIC_COMMAND_HEARTBEAT +#undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH + +#undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE +#undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND +#undef DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION +#undef DMAPI5_TOPIC_PARAMETER_NEW_PORT +#undef DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE +#undef DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME +#undef DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE +#undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION + +#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE +#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE +#undef DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES + +#undef DMAPI5_REVISION_INFORMATION_COMMIT_SHA +#undef DMAPI5_REVISION_INFORMATION_TIMESTAMP +#undef DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA + +#undef DMAPI5_CHAT_USER_ID +#undef DMAPI5_CHAT_USER_FRIENDLY_NAME +#undef DMAPI5_CHAT_USER_MENTION +#undef DMAPI5_CHAT_USER_CHANNEL + +#undef DMAPI5_CHAT_CHANNEL_ID +#undef DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME +#undef DMAPI5_CHAT_CHANNEL_CONNECTION_NAME +#undef DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL +#undef DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL +#undef DMAPI5_CHAT_CHANNEL_TAG +#undef DMAPI5_CHAT_CHANNEL_EMBEDS_SUPPORTED + +#undef DMAPI5_CUSTOM_CHAT_COMMAND_NAME +#undef DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT +#undef DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY diff --git a/nebula.dme b/nebula.dme index 4227eb44340..aafc0189cbb 100644 --- a/nebula.dme +++ b/nebula.dme @@ -99,6 +99,8 @@ #include "code\__defines\targeting.dm" #include "code\__defines\temperature.dm" #include "code\__defines\template_tags.dm" +#include "code\__defines\tgs.config.dm" +#include "code\__defines\tgs.dm" #include "code\__defines\time.dm" #include "code\__defines\tools.dm" #include "code\__defines\topic.dm" @@ -3783,6 +3785,7 @@ #include "code\modules\synthesized_instruments\real_instruments\Synthesizer\synthesizer.dm" #include "code\modules\synthesized_instruments\real_instruments\Trumpet\trumpet.dm" #include "code\modules\synthesized_instruments\real_instruments\Violin\violin.dm" +#include "code\modules\tgs\includes.dm" #include "code\modules\tools\tool.dm" #include "code\modules\tools\archetypes\_tool_defines.dm" #include "code\modules\tools\archetypes\tool_archetype.dm" diff --git a/tools/validate_dme.py b/tools/validate_dme.py index 4966ef550fe..d1761f2ac05 100644 --- a/tools/validate_dme.py +++ b/tools/validate_dme.py @@ -15,6 +15,8 @@ r'code/unit_tests/*.dm', # Ditto, todo: modularise or remove r'code/datums/music_tracks/*.dm', + # TGS handles importing itself due to API security reasons + r'code/modules/tgs/**/*.dm' ] lines = [] From 1d855d3d6a1cf0de7fef858f0c9f3c1e914f3f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=83=D0=BC=D0=B0=D1=81=D0=B0=D0=BD=D0=B4=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=20C=D0=B0=D0=BC=D0=B0=D0=BB=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=91=D0=B0=D0=B1=D0=B0=D0=BD?= Date: Sun, 9 Jun 2024 22:26:37 +0700 Subject: [PATCH 2/2] aoi --- code/modules/tgs/v5/api.dm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/modules/tgs/v5/api.dm b/code/modules/tgs/v5/api.dm index 36610b1242a..35544342da1 100644 --- a/code/modules/tgs/v5/api.dm +++ b/code/modules/tgs/v5/api.dm @@ -160,7 +160,7 @@ if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]") if(event_handler != null) event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) @@ -201,7 +201,7 @@ if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]") server_port = new_port return TopicResponse() @@ -212,7 +212,7 @@ var/error_message = null if (new_port != null) if (!isnum(new_port) || !(new_port > 0)) - error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]" + error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]" else server_port = new_port