diff --git a/.tgs.yml b/.tgs.yml new file mode 100644 index 00000000..90cf9730 --- /dev/null +++ b/.tgs.yml @@ -0,0 +1,25 @@ +# This file is used by TGS (https://github.com/tgstation/tgstation-server) clients to quickly initialize a server instance for the codebase +# The format isn't documented anywhere but hopefully we never have to change it. If there are questions, contact the TGS maintainer Cyberboss/@Dominion#0444 +version: 1 +# The BYOND version to use (kept in sync with dependencies.sh by the "TGS Test Suite" CI job) +# Must be interpreted as a string, keep quoted +byond: "514.1589" +# Folders to create in "/Configuration/GameStaticFiles/" +static_files: + # Config directory should be static + - name: config + # This implies the folder should be pre-populated with contents from the repo + populate: true + # Data directory must be static + - name: data +# String dictionary. The value is the location of the file in the repo to upload to TGS. The key is the name of the file to upload to "/Configuration/EventScripts/" +# This one is for Linux hosted servers +#linux_scripts: +# PreCompile.sh: tools/tgs_scripts/PreCompile.sh +# WatchdogLaunch.sh: tools/tgs_scripts/WatchdogLaunch.sh +# InstallDeps.sh: tools/tgs_scripts/InstallDeps.sh +# Same as above for Windows hosted servers +#windows_scripts: +# PreCompile.bat: tools/tgs_scripts/PreCompile.bat +# The security level the game should be run at +security: Trusted diff --git a/code/__DEFINES/tgs.config.dm b/code/__DEFINES/tgs.config.dm index a40b5d46..3a3727fb 100644 --- a/code/__DEFINES/tgs.config.dm +++ b/code/__DEFINES/tgs.config.dm @@ -1,10 +1,12 @@ #define TGS_EXTERNAL_CONFIGURATION +#define TGS_V3_API #define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) GLOBAL_VAR_INIT(##Name, ##Value); GLOBAL_PROTECT(##Name) #define TGS_READ_GLOBAL(Name) GLOB.##Name #define TGS_WRITE_GLOBAL(Name, Value) GLOB.##Name = ##Value #define TGS_WORLD_ANNOUNCE(message) to_chat(world, "[html_encode(##message)]") -#define TGS_INFO_LOG(message) log_world("TGS: Info: [##message]") -#define TGS_ERROR_LOG(message) log_world("TGS: Error: [##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) stack_trace("TGS Error: [##message]") #define TGS_NOTIFY_ADMINS(event) message_admins(##event) #define TGS_CLIENT_COUNT GLOB.clients.len #define TGS_PROTECT_DATUM(Path) GENERAL_PROTECT_DATUM(##Path) diff --git a/code/__DEFINES/tgs.dm b/code/__DEFINES/tgs.dm index db4f046e..bdd66a28 100644 --- a/code/__DEFINES/tgs.dm +++ b/code/__DEFINES/tgs.dm @@ -1,242 +1,518 @@ -//tgstation-server DMAPI - -//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 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` -//These globals must not be modifiable from anywhere outside of the server tools -#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 priveledge escalation -#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 error `message` to a server log -#define TGS_ERROR_LOG(message) - -//Get the number of connected /clients -#define TGS_CLIENT_COUNT - -#endif - -//EVENT CODES - -#define TGS_EVENT_PORT_SWAP -2 //before a port change is about to happen, extra parameter is new port -#define TGS_EVENT_REBOOT_MODE_CHANGE -1 //before a reboot mode change, extras parameters are the current and new reboot mode enums - -//See the descriptions for these codes here: https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/EventType.cs -#define TGS_EVENT_REPO_RESET_ORIGIN 0 -#define TGS_EVENT_REPO_CHECKOUT 1 -#define TGS_EVENT_REPO_FETCH 2 -#define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3 -#define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4 -#define TGS_EVENT_BYOND_INSTALL_START 5 -#define TGS_EVENT_BYOND_INSTALL_FAIL 6 -#define TGS_EVENT_BYOND_ACTIVE_VERSION_CHANGE 7 -#define TGS_EVENT_COMPILE_START 8 -#define TGS_EVENT_COMPILE_CANCELLED 9 -#define TGS_EVENT_COMPILE_FAILURE 10 -#define TGS_EVENT_COMPILE_COMPLETE 11 -#define TGS_EVENT_INSTANCE_AUTO_UPDATE_START 12 -#define TGS_EVENT_REPO_MERGE_CONFLICT 13 - -//OTHER ENUMS - -#define TGS_REBOOT_MODE_NORMAL 0 -#define TGS_REBOOT_MODE_SHUTDOWN 1 -#define TGS_REBOOT_MODE_RESTART 2 - -#define TGS_SECURITY_TRUSTED 0 -#define TGS_SECURITY_SAFE 1 -#define TGS_SECURITY_ULTRASAFE 2 - -//REQUIRED HOOKS - -//Call this somewhere in /world/New() that is always run -//event_handler: optional user defined event handler. The default behaviour is to broadcast the event in english to all connected admin channels -//minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated -/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.sleep_offline to make this happen so ensure no changes are made to it while this call is running -//Most importantly, before this point, note that any static files or directories may be in use by another server. Your code should account for this -//This function should not be called before ..() in /world/New() -/world/proc/TgsInitializationComplete() - return - -//Put this at the start of /world/Topic() -#define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return - -//Call this at the beginning of world/Reboot(reason) -/world/proc/TgsReboot() - return - -//DATUM DEFINITIONS -//unless otherwise specified all datums defined here should be considered read-only, warranty void if written - -//represents git revision information about the current world build -/datum/tgs_revision_information - var/commit //full sha of compiled commit - var/origin_commit //full sha of last known remote commit. This may be null if the TGS repository is not currently tracking a remote branch - -//represents a merge of a GitHub pull request -/datum/tgs_revision_information/test_merge - var/number //pull request number - var/title //pull request title - var/body //pull request body - var/author //pull request github author - var/url //link to pull request html - var/pull_request_commit //commit of the pull request when it was merged - var/time_merged //timestamp of when the merge commit for the pull request was created - var/comment //optional comment left by the one who initiated the test merge - -//represents a connected chat channel -/datum/tgs_chat_channel - var/id //internal channel representation - var/friendly_name //user friendly channel name - var/connection_name //the name of the configured chat connection - var/is_admin_channel //if the server operator has marked this channel for game admins only - var/is_private_channel //if this is a private chat channel - var/custom_tag //user defined string associated with channel - -//represents a chat user -/datum/tgs_chat_user - var/id //Internal user representation, requires channel to be unique - var/friendly_name //The user's public name - var/mention //The text to use to ping this user in a message - var/datum/tgs_chat_channel/channel //The /datum/tgs_chat_channel this user was from - -//user definable callback for handling events -//extra parameters may be specified depending on the event -/datum/tgs_event_handler/proc/HandleEvent(event_code, ...) - set waitfor = FALSE - return - -//user definable chat command -/datum/tgs_chat_command - var/name = "" //the string to trigger this command on a chat bot. e.g. TGS3_BOT: do_this_command - var/help_text = "" //help text for this command - var/admin_only = FALSE //set to TRUE if this command should only be usable by registered chat admins - -//override to implement command -//sender: The tgs_chat_user who send to command -//params: The trimmed string following the command name -//The return value will be stringified and sent to the appropriate chat -/datum/tgs_chat_command/proc/Run(datum/tgs_chat_user/sender, params) - CRASH("[type] has no implementation for Run()") - -//FUNCTIONS - -//Returns the respective string version of the API -/world/proc/TgsMaximumAPIVersion() - return - -/world/proc/TgsMinimumAPIVersion() - return - -//Gets the current version of the server tools running the server -/world/proc/TgsVersion() - return - -//Returns TRUE if the world was launched under the server tools and the API matches, FALSE otherwise -//No function below this succeeds if it returns FALSE -/world/proc/TgsAvailable() - return - -/world/proc/TgsInstanceName() - return - -//Get the current `/datum/tgs_revision_information` -/world/proc/TgsRevision() - return - -//Get the current BYOND security level -/world/proc/TgsSecurityLevel() - return - -//Gets a list of active `/datum/tgs_revision_information/test_merge`s -/world/proc/TgsTestMerges() - return - -//Forces a hard reboot of BYOND by ending the process -//unlike del(world) clients will try to reconnect -//If the service has not requested a shutdown, the next server will take over -/world/proc/TgsEndProcess() - return - -//Gets a list of connected tgs_chat_channel -/world/proc/TgsChatChannelInfo() - return - -//Sends a message to connected game chats -//message: The message to send -//channels: optional channels to limit the broadcast to -/world/proc/TgsChatBroadcast(message, list/channels) - return - -//Send a message to non-admin connected chats -//message: The message to send -//admin_only: If TRUE, message will instead be sent to only admin connected chats -/world/proc/TgsTargetedChatBroadcast(message, admin_only) - return - -//Send a private message to a specific user -//message: The message to send -//user: The /datum/tgs_chat_user to send to -/world/proc/TgsChatPrivateMessage(message, datum/tgs_chat_user/user) - 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. -*/ \ No newline at end of file +// tgstation-server DMAPI + +#define TGS_DMAPI_VERSION "7.0.2" + +// 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 watchdog sends a health check to DD. No parameters. +#define TGS_EVENT_HEALTH_CHECK -5 + +/// 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 engine install operation begins. Parameters: Version string of the installing engine. +#define TGS_EVENT_ENGINE_INSTALL_START 5 +/// When a engine install operation fails. Parameters: Error message +#define TGS_EVENT_ENGINE_INSTALL_FAIL 6 +/// When the active engine version changes. Parameters: (Nullable) Version string of the current engine, version string of the new engine. +#define TGS_EVENT_ENGINE_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 +/// After a single submodule update is performed. Parameters: Updated submodule name. +#define TGS_EVENT_REPO_SUBMODULE_UPDATE 23 +/// After CodeModifications are applied, before DreamMaker is run. Parameters: Game directory path, origin commit sha, version string of the used engine. +#define TGS_EVENT_PRE_DREAM_MAKER 24 +/// Whenever a deployment folder is deleted from disk. Parameters: Game directory path. +#define TGS_EVENT_DEPLOYMENT_CLEANUP 25 + +// 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 + +// Note that security levels are currently meaningless in OpenDream +/// 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 + +/// DreamDaemon public visibility level. +#define TGS_VISIBILITY_PUBLIC 0 +/// DreamDaemon private visibility level. +#define TGS_VISIBILITY_PRIVATE 1 +/// DreamDaemon invisible visibility level. +#define TGS_VISIBILITY_INVISIBLE 2 + +/// The Build Your Own Net Dream engine. +#define TGS_ENGINE_TYPE_BYOND 0 +/// The OpenDream engine. +#define TGS_ENGINE_TYPE_OPENDREAM 1 + +//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] (BEFORE ..()). + */ +/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 handler for TGS events. +/datum/tgs_event_handler + /// If the handler receieves [TGS_EVENT_HEALTH_CHECK] events. + var/receive_health_checks = FALSE + +/** + * 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. This function may sleep! + * + * 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. This function may sleep! + * If TGS is offline when called, the message may be placed in a queue to be sent and this function will return immediately. Your message will be sent when TGS reconnects to the game. + * + * 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. This function may sleep! + * If TGS is offline when called, the message may be placed in a queue to be sent and this function will return immediately. Your message will be sent when TGS reconnects to the game. + * + * 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 + +/** + * Send a message to connected chats that are flagged as game-related in TGS. This function may sleep! + * If TGS is offline when called, the message may be placed in a queue to be sent and this function will return immediately. Your message will be sent when TGS reconnects to the game. + * + * 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. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsVersion() + return + +/// Returns the running engine type +/world/proc/TgsEngine() + return + +/// Returns the current [/datum/tgs_version] of the DMAPI being used if it was activated, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsApiVersion() + return + +/// Returns the name of the TGS instance running the game if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsInstanceName() + return + +/// Return the current [/datum/tgs_revision_information] of the running server if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsRevision() + return + +/// Returns the current BYOND security level as a TGS_SECURITY_ define if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsSecurityLevel() + return + +/// Returns the current BYOND visibility level as a TGS_VISIBILITY_ define if TGS is present, null otherwise. Requires TGS to be using interop API version 5 or higher otherwise the string "___unimplemented" wil be returned. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsVisibility() + return + +/// Returns a list of active [/datum/tgs_revision_information/test_merge]s if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsTestMerges() + return + +/// Returns a list of connected [/datum/tgs_chat_channel]s if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsChatChannelInfo() + return + +/* +The MIT License + +Copyright (c) 2017-2023 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/datums/helper_datums/getrev.dm b/code/datums/helper_datums/getrev.dm index 7d040a49..ca5ea28e 100644 --- a/code/datums/helper_datums/getrev.dm +++ b/code/datums/helper_datums/getrev.dm @@ -42,10 +42,8 @@ . = header ? "The following pull requests are currently test merged:
" : "" for(var/line in testmerge) var/datum/tgs_revision_information/test_merge/tm = line - var/cm = tm.pull_request_commit + var/cm = tm.head_commit var/details = ": '" + html_encode(tm.title) + "' by " + html_encode(tm.author) + " at commit " + html_encode(copytext_char(cm, 1, 11)) - if(details && findtext(details, "\[s\]") && (!usr || !usr.client.holder)) - continue . += "#[tm.number][details]
" /client/verb/showrevinfo() @@ -119,4 +117,4 @@ if(probabilities[ctag] > 0) var/percentage = round(probabilities[ctag] / sum * 100, 0.1) msg += "[ctag] [percentage]%" - to_chat(src, msg.Join("
")) \ No newline at end of file + to_chat(src, msg.Join("
")) diff --git a/code/modules/tgs/core/_definitions.dm b/code/modules/tgs/core/_definitions.dm index ebf6d17c..e8a041cd 100644 --- a/code/modules/tgs/core/_definitions.dm +++ b/code/modules/tgs/core/_definitions.dm @@ -1,2 +1,10 @@ -#define TGS_UNIMPLEMENTED "___unimplemented" -#define TGS_VERSION_PARAMETER "server_service_version" +#if DM_VERSION < 510 +#error The TGS DMAPI does not support BYOND versions < 510! +#endif + +#define TGS_UNIMPLEMENTED "___unimplemented" +#define TGS_VERSION_PARAMETER "server_service_version" + +#ifndef TGS_DEBUG_LOG +#define TGS_DEBUG_LOG(message) +#endif diff --git a/code/modules/tgs/core/core.dm b/code/modules/tgs/core/core.dm index 61f1e71d..b5029c57 100644 --- a/code/modules/tgs/core/core.dm +++ b/code/modules/tgs/core/core.dm @@ -1,144 +1,168 @@ -/world/TgsNew(datum/tgs_event_handler/event_handler) - var/tgs_version = world.params[TGS_VERSION_PARAMETER] - if(!tgs_version) - return - - var/path = SelectTgsApi(tgs_version) - if(!path) - TGS_ERROR_LOG("Found unsupported API version: [tgs_version]. If this is a valid version please report this, backporting is done on demand.") - - TGS_INFO_LOG("Activating API for version [tgs_version]") - var/datum/tgs_api/new_api = new path - - var/result = new_api.OnWorldNew(event_handler ? event_handler : new /datum/tgs_event_handler/tgs_default) - if(result && result != TGS_UNIMPLEMENTED) - TGS_WRITE_GLOBAL(tgs, new_api) - else - TGS_ERROR_LOG("Failed to activate API!") - -/world/proc/SelectTgsApi(tgs_version) - //remove the old 3.0 header - tgs_version = replacetext(tgs_version, "/tg/station 13 Server v", "") - - var/list/version_bits = splittext(tgs_version, ".") - - var/super = text2num(version_bits[1]) - var/major = text2num(version_bits[2]) - var/minor = text2num(version_bits[3]) - var/patch = text2num(version_bits[4]) - - switch(super) - if(3) - switch(major) - if(2) - return /datum/tgs_api/v3210 - - if(super != null && major != null && minor != null && patch != null && tgs_version > TgsMaximumAPIVersion()) - TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.") - return /datum/tgs_api/latest - -/world/TgsMaximumAPIVersion() - return "4.0.0.0" - -/world/TgsMinimumAPIVersion() - return "3.2.0.0" - -/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() - return world.params[TGS_VERSION_PARAMETER] - -/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) - -/* -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. -*/ +/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 Interop 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 Interop 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/TgsEngine() +#ifdef OPENDREAM + return TGS_ENGINE_TYPE_OPENDREAM +#else + return TGS_ENGINE_TYPE_BYOND +#endif + +/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) + return api.SecurityLevel() + +/world/TgsVisibility() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + return api.Visibility() diff --git a/code/modules/tgs/core/datum.dm b/code/modules/tgs/core/datum.dm index 6af39a75..d2c7a6d3 100644 --- a/code/modules/tgs/core/datum.dm +++ b/code/modules/tgs/core/datum.dm @@ -1,74 +1,71 @@ -TGS_DEFINE_AND_SET_GLOBAL(tgs, null) - -/datum/tgs_api - -/datum/tgs_api/latest - parent_type = /datum/tgs_api/v3210 - -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, admin_only) - return TGS_UNIMPLEMENTED - -/* -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. -*/ +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/proc/TerminateWorld() + while(TRUE) + TGS_DEBUG_LOG("About to terminate world. Tick: [world.time], sleep_offline: [world.sleep_offline]") + world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866 + del(world) + world.sleep_offline = FALSE // just in case, this is BYOND after all... + sleep(1) + TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]") + +/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 + +/datum/tgs_api/proc/Visibility() + 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 00000000..6d19cdb2 --- /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 index 7ca906c8..a8c84bb7 100644 --- a/code/modules/tgs/includes.dm +++ b/code/modules/tgs/includes.dm @@ -1,6 +1,21 @@ -#include "core\_definitions.dm" -#include "core\core.dm" -#include "core\datum.dm" -#include "core\default_event_handler.dm" -#include "v3210\api.dm" -#include "v3210\commands.dm" +#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\bridge.dm" +#include "v5\chunking.dm" +#include "v5\commands.dm" +#include "v5\serializers.dm" +#include "v5\topic.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 00000000..c50faf75 --- /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 index 2a95f786..5f1cb320 100644 --- a/code/modules/tgs/v3210/api.dm +++ b/code/modules/tgs/v3210/api.dm @@ -1,248 +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" - -/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 "3.2.1.0" - -/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/proc/file2list(filename) - return splittext(trim_left(trim_right(file2text(filename))), "\n") - -/datum/tgs_api/v3210/OnWorldNew(datum/tgs_event_handler/event_handler) //don't use event handling in this version - . = 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 = file2list(".git/logs/HEAD") - if(logs.len) - logs = splittext(logs[logs.len - 1], " ") - commit = logs[2] - logs = file2list(".git/logs/refs/remotes/origin/master") - if(logs.len) - originmastercommit = splittext(logs[logs.len - 1], " ")[2] - - 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) - ExportService("[SERVICE_REQUEST_API_VERSION] [ApiVersion()]", 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 - call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval - 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.pull_request_commit = entry["commit"] - tm.author = entry["author"] - tm.title = entry["title"] - . += tm - -/datum/tgs_api/v3210/Revision() - if(!warned_revison) - TGS_ERROR_LOG("Use of TgsRevision on [ApiVersion()] origin_commit only points to master!") - warned_revison = TRUE - var/datum/tgs_revision_information/ri = new - ri.commit = commit - ri.origin_commit = originmastercommit - -/datum/tgs_api/v3210/EndProcess() - sleep(world.tick_lag) //flush the buffers - ExportService(SERVICE_REQUEST_KILL_PROCESS) - -/datum/tgs_api/v3210/ChatChannelInfo() - return list() - -/datum/tgs_api/v3210/ChatBroadcast(message, list/channels) - if(channels) - return TGS_UNIMPLEMENTED - ChatTargetedBroadcast(message, TRUE) - ChatTargetedBroadcast(message, FALSE) - -/datum/tgs_api/v3210/ChatTargetedBroadcast(message, admin_only) - ExportService("[admin_only ? SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE : SERVICE_REQUEST_IRC_BROADCAST] [message]") - -/datum/tgs_api/v3210/ChatPrivateMessage(message, admin_only) - return TGS_UNIMPLEMENTED - -#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 - -/* -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. -*/ +#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_WARNING_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 index 50466319..6961eea4 100644 --- a/code/modules/tgs/v3210/commands.dm +++ b/code/modules/tgs/v3210/commands.dm @@ -1,78 +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_ERROR_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 - 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 - user.mention = sender - return stc.Run(user, params) || TRUE - -/* - -#undef SERVICE_JSON_PARAM_HELPTEXT -#undef SERVICE_JSON_PARAM_ADMINONLY -#undef SERVICE_JSON_PARAM_REQUIREDPARAMETERS - -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. -*/ +#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 ? result.text : TRUE diff --git a/code/modules/tgs/v4/api.dm b/code/modules/tgs/v4/api.dm new file mode 100644 index 00000000..6dbb9a7d --- /dev/null +++ b/code/modules/tgs/v4/api.dm @@ -0,0 +1,322 @@ +#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]")) + TerminateWorld() + + 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]") + TerminateWorld() + + //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]") + TerminateWorld() + + 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]") + TerminateWorld() + + if(new_port != world.port && !world.OpenPort(new_port)) + TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]") + TerminateWorld() + 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) + + if (!length(channels)) + return + + 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) + + if (!length(channels)) + return + + 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 00000000..e9ff27d4 --- /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 ? result.text : TRUE + return "Unknown command: [command]!" diff --git a/code/modules/tgs/v5/__interop_version.dm b/code/modules/tgs/v5/__interop_version.dm new file mode 100644 index 00000000..cafe74bb --- /dev/null +++ b/code/modules/tgs/v5/__interop_version.dm @@ -0,0 +1 @@ +"5.8.0" diff --git a/code/modules/tgs/v5/_defines.dm b/code/modules/tgs/v5/_defines.dm new file mode 100644 index 00000000..de8b1389 --- /dev/null +++ b/code/modules/tgs/v5/_defines.dm @@ -0,0 +1,118 @@ +#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_REQUEST_LIMIT 8198 +#define DMAPI5_TOPIC_REQUEST_LIMIT 65528 +#define DMAPI5_TOPIC_RESPONSE_LIMIT 65529 + +#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_BRIDGE_COMMAND_CHUNK 6 + +#define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" +#define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" +#define DMAPI5_PARAMETER_TOPIC_PORT "topicPort" + +#define DMAPI5_CHUNK "chunk" +#define DMAPI5_CHUNK_PAYLOAD "payload" +#define DMAPI5_CHUNK_TOTAL "totalChunks" +#define DMAPI5_CHUNK_SEQUENCE_ID "sequenceId" +#define DMAPI5_CHUNK_PAYLOAD_ID "payloadId" + +#define DMAPI5_MISSING_CHUNKS "missingChunks" + +#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_RUNTIME_INFORMATION_VISIBILITY "visibility" + +#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_HEALTHCHECK 7 +#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8 +#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9 +#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10 +#define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11 + +#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_PARAMETER_BROADCAST_MESSAGE "broadcastMessage" + +#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 00000000..25e00aed --- /dev/null +++ b/code/modules/tgs/v5/api.dm @@ -0,0 +1,281 @@ +/datum/tgs_api/v5 + var/server_port + var/access_identifier + + var/instance_name + var/security_level + var/visibility + + var/reboot_mode = TGS_REBOOT_MODE_NORMAL + + /// List of chat messages list()s that attempted to be sent during a topic call. To be bundled in the result of the call + var/list/intercepted_message_queue + + /// List of chat messages list()s that attempted to be sent during a topic call. To be bundled in the result of the call + var/list/offline_message_queue + + var/list/custom_commands + + var/list/test_merges + var/datum/tgs_revision_information/revision + var/list/chat_channels + + var/initialized = FALSE + var/initial_bridge_request_received = FALSE + var/datum/tgs_version/interop_version + + var/chunked_requests = 0 + var/list/chunked_topics = list() + + var/detached = FALSE + +/datum/tgs_api/v5/New() + . = ..() + interop_version = version + TGS_DEBUG_LOG("V5 API created: [json_encode(args)]") + +/datum/tgs_api/v5/ApiVersion() + return new /datum/tgs_version( + #include "__interop_version.dm" + ) + +/datum/tgs_api/v5/OnWorldNew(minimum_required_security_level) + TGS_DEBUG_LOG("OnWorldNew()") + 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 // we want this to be the TGS version, not the interop version + 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(), DMAPI5_PARAMETER_TOPIC_PORT = GetTopicPort())) + 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...") + TerminateWorld() + + initial_bridge_request_received = TRUE + version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) // reassigning this because it can change if TGS updates + security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL] + visibility = runtime_information[DMAPI5_RUNTIME_INFORMATION_VISIBILITY] + 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/GetTopicPort() +#if defined(OPENDREAM) && defined(OPENDREAM_TOPIC_PORT_EXISTS) + return "[world.opendream_topic_port]" +#else + return null +#endif + +/datum/tgs_api/v5/proc/RequireInitialBridgeResponse() + TGS_DEBUG_LOG("RequireInitialBridgeResponse()") + var/logged = FALSE + while(!initial_bridge_request_received) + if(!logged) + TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep") + logged = TRUE + + sleep(1) + + TGS_DEBUG_LOG("RequireInitialBridgeResponse: Passed") + +/datum/tgs_api/v5/OnInitializationComplete() + Bridge(DMAPI5_BRIDGE_COMMAND_PRIME) + +/datum/tgs_api/v5/OnTopic(T) + TGS_DEBUG_LOG("OnTopic()") + RequireInitialBridgeResponse() + TGS_DEBUG_LOG("OnTopic passed bridge request gate") + var/list/params = params2list(T) + var/json = params[DMAPI5_TOPIC_DATA] + if(!json) + TGS_DEBUG_LOG("No \"[DMAPI5_TOPIC_DATA]\" entry found, ignoring...") + return FALSE // continue to /world/Topic + + if(!initialized) + TGS_WARNING_LOG("Missed topic due to not being initialized: [json]") + return TRUE // too early to handle, but it's still our responsibility + + return ProcessTopicJson(json, TRUE) + +/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/message2, 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 + + SendChatMessageRaw(message2, ids) + +/datum/tgs_api/v5/ChatTargetedBroadcast(datum/tgs_message_content/message2, 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 + + SendChatMessageRaw(message2, channels) + +/datum/tgs_api/v5/ChatPrivateMessage(datum/tgs_message_content/message2, datum/tgs_chat_user/user) + SendChatMessageRaw(message2, list(user.channel.id)) + +/datum/tgs_api/v5/proc/SendChatMessageRaw(datum/tgs_message_content/message2, list/channel_ids) + message2 = UpgradeDeprecatedChatMessage(message2) + + if (!length(channel_ids)) + return + + var/list/data = message2._interop_serialize() + data[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = channel_ids + if(intercepted_message_queue) + intercepted_message_queue += list(data) + return + + if(offline_message_queue) + offline_message_queue += list(data) + return + + if(detached) + offline_message_queue = list(data) + + WaitForReattach(FALSE) + + data = offline_message_queue + offline_message_queue = null + + for(var/queued_message in data) + SendChatDataRaw(queued_message) + else + SendChatDataRaw(data) + +/datum/tgs_api/v5/proc/SendChatDataRaw(list/data) + Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = data)) + +/datum/tgs_api/v5/ChatChannelInfo() + RequireInitialBridgeResponse() + WaitForReattach(TRUE) + return chat_channels.Copy() + +/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json) + TGS_DEBUG_LOG("DecodeChannels()") + 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 + +/datum/tgs_api/v5/Visibility() + RequireInitialBridgeResponse() + return visibility diff --git a/code/modules/tgs/v5/bridge.dm b/code/modules/tgs/v5/bridge.dm new file mode 100644 index 00000000..a261f8a0 --- /dev/null +++ b/code/modules/tgs/v5/bridge.dm @@ -0,0 +1,106 @@ +/datum/tgs_api/v5/proc/Bridge(command, list/data) + if(!data) + data = list() + + var/single_bridge_request = CreateBridgeRequest(command, data) + if(length(single_bridge_request) <= DMAPI5_BRIDGE_REQUEST_LIMIT) + return PerformBridgeRequest(single_bridge_request) + + // chunking required + var/payload_id = ++chunked_requests + + var/raw_data = CreateBridgeData(command, data, FALSE) + + var/list/chunk_requests = GenerateChunks(raw_data, TRUE) + + var/list/response + for(var/bridge_request in chunk_requests) + response = PerformBridgeRequest(bridge_request) + if(!response) + // Abort + return + + var/list/missing_sequence_ids = response[DMAPI5_MISSING_CHUNKS] + if(length(missing_sequence_ids)) + do + TGS_WARNING_LOG("Server is still missing some chunks of bridge P[payload_id]! Sending missing chunks...") + if(!istype(missing_sequence_ids)) + TGS_ERROR_LOG("Did not receive a list() for [DMAPI5_MISSING_CHUNKS]!") + return + + for(var/missing_sequence_id in missing_sequence_ids) + if(!isnum(missing_sequence_id)) + TGS_ERROR_LOG("Did not receive a num in [DMAPI5_MISSING_CHUNKS]!") + return + + var/missing_chunk_request = chunk_requests[missing_sequence_id + 1] + response = PerformBridgeRequest(missing_chunk_request) + if(!response) + // Abort + return + + missing_sequence_ids = response[DMAPI5_MISSING_CHUNKS] + while(length(missing_sequence_ids)) + + return response + +/datum/tgs_api/v5/proc/CreateBridgeRequest(command, list/data) + var/json = CreateBridgeData(command, data, TRUE) + var/encoded_json = url_encode(json) + + var/api_prefix = interop_version.minor >= 8 ? "api/" : "" + + var/url = "http://127.0.0.1:[server_port]/[api_prefix]Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]" + return url + +/datum/tgs_api/v5/proc/CreateBridgeData(command, list/data, needs_auth) + data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command + if(needs_auth) + data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier + + var/json = json_encode(data) + return json + +/datum/tgs_api/v5/proc/WaitForReattach(require_channels = FALSE) + if(detached) + // Wait up to one minute + for(var/i in 1 to 600) + sleep(1) + if(!detached && (!require_channels || length(chat_channels))) + break + + // dad went out for milk and cigarettes 20 years ago... + // yes, this affects all other waiters, intentional + if(i == 600) + detached = FALSE + +/datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request) + WaitForReattach(FALSE) + + // This is an infinite sleep until we get a response + var/export_response = world.Export(bridge_request) + if(!export_response) + TGS_ERROR_LOG("Failed bridge request: [bridge_request]") + return + + var/content = export_response["CONTENT"] + if(!content) + TGS_ERROR_LOG("Failed bridge request, missing content!") + return + + var/response_json = file2text(content) + if(!response_json) + TGS_ERROR_LOG("Failed bridge request, failed to load content!") + return + + var/list/bridge_response = json_decode(response_json) + if(!bridge_response) + TGS_ERROR_LOG("Failed bridge request, bad json: [response_json]") + return + + var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE] + if(error) + TGS_ERROR_LOG("Failed bridge request, bad request: [error]") + return + + return bridge_response diff --git a/code/modules/tgs/v5/chunking.dm b/code/modules/tgs/v5/chunking.dm new file mode 100644 index 00000000..167a124b --- /dev/null +++ b/code/modules/tgs/v5/chunking.dm @@ -0,0 +1,43 @@ +/datum/tgs_api/v5/proc/GenerateChunks(payload, bridge) + var/limit = bridge ? DMAPI5_BRIDGE_REQUEST_LIMIT : DMAPI5_TOPIC_RESPONSE_LIMIT + + var/payload_id = ++chunked_requests + var/data_length = length(payload) + + var/chunk_count + var/list/chunk_requests + for(chunk_count = 2; !chunk_requests; ++chunk_count) + var/max_chunk_size = -round(-(data_length / chunk_count)) + if(max_chunk_size > limit) + continue + + chunk_requests = list() + for(var/i in 1 to chunk_count) + var/start_index = 1 + ((i - 1) * max_chunk_size) + if (start_index > data_length) + break + + var/end_index = min(1 + (i * max_chunk_size), data_length + 1) + + var/chunk_payload = copytext(payload, start_index, end_index) + + // sequence IDs in interop chunking are always zero indexed + var/list/chunk = list(DMAPI5_CHUNK_PAYLOAD_ID = payload_id, DMAPI5_CHUNK_SEQUENCE_ID = (i - 1), DMAPI5_CHUNK_TOTAL = chunk_count, DMAPI5_CHUNK_PAYLOAD = chunk_payload) + + var/chunk_request = list(DMAPI5_CHUNK = chunk) + var/chunk_length + if(bridge) + chunk_request = CreateBridgeRequest(DMAPI5_BRIDGE_COMMAND_CHUNK, chunk_request) + chunk_length = length(chunk_request) + else + chunk_request = list(chunk_request) // wrap for adding to list + chunk_length = length(json_encode(chunk_request)) + + if(chunk_length > limit) + // Screwed by encoding, no way to preempt it though + chunk_requests = null + break + + chunk_requests += chunk_request + + return chunk_requests diff --git a/code/modules/tgs/v5/commands.dm b/code/modules/tgs/v5/commands.dm new file mode 100644 index 00000000..c3ecf825 --- /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 = TopicResponse() + topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = response ? response.text : null + topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE] = response ? response._interop_serialize() : null + return 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 00000000..6782bc96 --- /dev/null +++ b/code/modules/tgs/v5/serializers.dm @@ -0,0 +1,59 @@ +/datum/tgs_message_content/proc/_interop_serialize() + return list("text" = text, "embed" = embed ? embed._interop_serialize() : null) + +/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(istype(fields, /list)) + 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" = src.image ? src.image._interop_serialize() : null, + "thumbnail" = thumbnail ? thumbnail._interop_serialize() : null, + "video" = video ? video._interop_serialize() : null, + "footer" = footer ? footer._interop_serialize() : null, + "provider" = provider ? provider._interop_serialize() : null, + "author" = author ? author._interop_serialize() : null, + "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/footer/_interop_serialize() + return list( + "text" = text, + "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/topic.dm b/code/modules/tgs/v5/topic.dm new file mode 100644 index 00000000..0a81adac --- /dev/null +++ b/code/modules/tgs/v5/topic.dm @@ -0,0 +1,283 @@ +/datum/tgs_api/v5/proc/TopicResponse(error_message = null) + var/list/response = list() + if(error_message) + response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message + return response + +/datum/tgs_api/v5/proc/ProcessTopicJson(json, check_access_identifier) + TGS_DEBUG_LOG("ProcessTopicJson(..., [check_access_identifier])") + var/list/result = ProcessRawTopic(json, check_access_identifier) + if(!result) + result = TopicResponse("Runtime error!") + else if(!length(result)) + return "{}" // quirk of json_encode is an empty list returns "[]" + + var/response_json = json_encode(result) + if(length(response_json) > DMAPI5_TOPIC_RESPONSE_LIMIT) + // cache response chunks and send the first + var/list/chunks = GenerateChunks(response_json, FALSE) + var/payload_id = chunks[1][DMAPI5_CHUNK][DMAPI5_CHUNK_PAYLOAD_ID] + var/cache_key = ResponseTopicChunkCacheKey(payload_id) + + chunked_topics[cache_key] = chunks + + response_json = json_encode(chunks[1]) + + return response_json + +/datum/tgs_api/v5/proc/ProcessRawTopic(json, check_access_identifier) + TGS_DEBUG_LOG("ProcessRawTopic(..., [check_access_identifier])") + var/list/topic_parameters = json_decode(json) + if(!topic_parameters) + TGS_DEBUG_LOG("ProcessRawTopic: json_decode failed") + return TopicResponse("Invalid topic parameters json: [json]!"); + + var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] + if(check_access_identifier && their_sCK != access_identifier) + TGS_DEBUG_LOG("ProcessRawTopic: access identifier check failed") + return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] or it does not match!") + + var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] + if(!isnum(command)) + TGS_DEBUG_LOG("ProcessRawTopic: command type check failed") + return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]!") + + return ProcessTopicCommand(command, topic_parameters) + +/datum/tgs_api/v5/proc/ResponseTopicChunkCacheKey(payload_id) + return "response[payload_id]" + +/datum/tgs_api/v5/proc/ProcessTopicCommand(command, list/topic_parameters) + TGS_DEBUG_LOG("ProcessTopicCommand([command], ...)") + switch(command) + + if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND) + intercepted_message_queue = list() + var/list/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) + if(!result) + result = TopicResponse("Error running chat command!") + result[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue + intercepted_message_queue = null + return result + + if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION) + 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)) + . = TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!") + else + var/list/response = TopicResponse() + . = response + if(event_handler != null) + var/list/event_call = list(event_type) + if(event_parameters) + event_call += event_parameters + + intercepted_message_queue = list() + event_handler.HandleEvent(arglist(event_call)) + response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue + intercepted_message_queue = null + + if (event_type == TGS_EVENT_WATCHDOG_DETACH) + detached = TRUE + chat_channels.Cut() // https://github.com/tgstation/tgstation-server/issues/1490 + + return + + 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) + TGS_DEBUG_LOG("ProcessTopicCommand: It's a chat update") + var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE] + if(!istype(chat_update_json)) + TGS_DEBUG_LOG("ProcessTopicCommand: failed \"[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]\" check") + 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_HEALTHCHECK) + if(event_handler && event_handler.receive_health_checks) + event_handler.HandleEvent(TGS_EVENT_HEALTH_CHECK) + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH) + detached = FALSE + 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 + + var/list/reattach_response = TopicResponse(error_message) + reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands() + reattach_response[DMAPI5_PARAMETER_TOPIC_PORT] = GetTopicPort() + return reattach_response + + if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK) + var/list/chunk = topic_parameters[DMAPI5_CHUNK] + if(!istype(chunk)) + return TopicResponse("Invalid [DMAPI5_CHUNK]!") + + var/payload_id = chunk[DMAPI5_CHUNK_PAYLOAD_ID] + if(!isnum(payload_id)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD_ID] is not a number!") + + // Always updated the highest known payload ID + chunked_requests = max(chunked_requests, payload_id) + + var/sequence_id = chunk[DMAPI5_CHUNK_SEQUENCE_ID] + if(!isnum(sequence_id)) + return TopicResponse("[DMAPI5_CHUNK_SEQUENCE_ID] is not a number!") + + var/total_chunks = chunk[DMAPI5_CHUNK_TOTAL] + if(!isnum(total_chunks)) + return TopicResponse("[DMAPI5_CHUNK_TOTAL] is not a number!") + + if(total_chunks == 0) + return TopicResponse("[DMAPI5_CHUNK_TOTAL] is zero!") + + var/payload = chunk[DMAPI5_CHUNK_PAYLOAD] + if(!istext(payload)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD] is not text!") + + var/cache_key = "request[payload_id]" + var/payloads = chunked_topics[cache_key] + + if(!payloads) + payloads = new /list(total_chunks) + chunked_topics[cache_key] = payloads + + if(total_chunks != length(payloads)) + chunked_topics -= cache_key + return TopicResponse("Received differing total chunks for same [DMAPI5_CHUNK_PAYLOAD_ID]! Invalidating [DMAPI5_CHUNK_PAYLOAD_ID]!") + + var/pre_existing_chunk = payloads[sequence_id + 1] + if(pre_existing_chunk && pre_existing_chunk != payload) + chunked_topics -= cache_key + return TopicResponse("Received differing payload for same [DMAPI5_CHUNK_SEQUENCE_ID]! Invalidating [DMAPI5_CHUNK_PAYLOAD_ID]!") + + payloads[sequence_id + 1] = payload + + var/list/missing_sequence_ids = list() + for(var/i in 1 to total_chunks) + if(!payloads[i]) + missing_sequence_ids += i - 1 + + if(length(missing_sequence_ids)) + return list(DMAPI5_MISSING_CHUNKS = missing_sequence_ids) + + chunked_topics -= cache_key + var/full_json = jointext(payloads, "") + + return ProcessRawTopic(full_json, FALSE) + + if(DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK) + var/payload_id = topic_parameters[DMAPI5_CHUNK_PAYLOAD_ID] + if(!isnum(payload_id)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD_ID] is not a number!") + + // Always updated the highest known payload ID + chunked_requests = max(chunked_requests, payload_id) + + var/list/missing_chunks = topic_parameters[DMAPI5_MISSING_CHUNKS] + if(!istype(missing_chunks) || !length(missing_chunks)) + return TopicResponse("Missing or empty [DMAPI5_MISSING_CHUNKS]!") + + var/sequence_id_to_send = missing_chunks[1] + if(!isnum(sequence_id_to_send)) + return TopicResponse("[DMAPI5_MISSING_CHUNKS] contained a non-number!") + + var/cache_key = ResponseTopicChunkCacheKey(payload_id) + var/list/chunks = chunked_topics[cache_key] + if(!chunks) + return TopicResponse("Unknown response chunk set: P[payload_id]!") + + // sequence IDs in interop chunking are always zero indexed + var/chunk_to_send = chunks[sequence_id_to_send + 1] + if(!chunk_to_send) + return TopicResponse("Sequence ID [sequence_id_to_send] is not present in response chunk P[payload_id]!") + + if(length(missing_chunks) == 1) + // sending last chunk, purge the cache + chunked_topics -= cache_key + + return chunk_to_send + + if(DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST) + var/message = topic_parameters[DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE] + if (!istext(message)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE]") + + TGS_WORLD_ANNOUNCE(message) + return TopicResponse() + + return TopicResponse("Unknown command: [command]") + +/datum/tgs_api/v5/proc/WorldBroadcast(message) + set waitfor = FALSE + TGS_WORLD_ANNOUNCE(message) diff --git a/code/modules/tgs/v5/undefs.dm b/code/modules/tgs/v5/undefs.dm new file mode 100644 index 00000000..94dcb2e4 --- /dev/null +++ b/code/modules/tgs/v5/undefs.dm @@ -0,0 +1,118 @@ +#undef DMAPI5_PARAM_SERVER_PORT +#undef DMAPI5_PARAM_ACCESS_IDENTIFIER + +#undef DMAPI5_BRIDGE_DATA +#undef DMAPI5_TOPIC_DATA + +#undef DMAPI5_BRIDGE_REQUEST_LIMIT +#undef DMAPI5_TOPIC_REQUEST_LIMIT +#undef DMAPI5_TOPIC_RESPONSE_LIMIT + +#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_BRIDGE_COMMAND_CHUNK + +#undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER +#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS +#undef DMAPI5_PARAMETER_TOPIC_PORT + +#undef DMAPI5_CHUNK +#undef DMAPI5_CHUNK_PAYLOAD +#undef DMAPI5_CHUNK_TOTAL +#undef DMAPI5_CHUNK_SEQUENCE_ID +#undef DMAPI5_CHUNK_PAYLOAD_ID + +#undef DMAPI5_MISSING_CHUNKS + +#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_RUNTIME_INFORMATION_VISIBILITY + +#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_HEALTHCHECK +#undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH +#undef DMAPI5_TOPIC_COMMAND_SEND_CHUNK +#undef DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK +#undef DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST + +#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_PARAMETER_BROADCAST_MESSAGE + +#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