From b1ce79784d1dfe4502e6e1a1637465558db15f19 Mon Sep 17 00:00:00 2001 From: king5327 Date: Wed, 26 Apr 2023 18:57:40 -0400 Subject: [PATCH 1/6] Adds TGS tweaks. --- code/game/world.dm | 1 + code/modules/client/client_procs.dm | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/code/game/world.dm b/code/game/world.dm index 475a6f76c5a..809185f2a66 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -166,6 +166,7 @@ var/world_topic_spam_protect_ip = "0.0.0.0" var/world_topic_spam_protect_time = world.timeofday /world/Topic(T, addr, master, key) + TGS_TOPIC var/list/topic_handlers = WorldTopicHandlers() var/list/input = params2list(T) diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index b3d3af59dfd..7889a44743e 100755 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -249,6 +249,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( . = ..() //calls mob.Login() + ++global.client_count if (byond_version >= 512) if (!byond_build || byond_build < 1386) message_admins(span_adminnotice("[key_name(src)] has been detected as spoofing their byond version. Connection rejected.")) @@ -330,7 +331,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( /client/Del() if(!gc_destroyed) Destroy() //Clean up signals and timers. - return ..() + . = ..() + --global.client_count /client/Destroy() clients -= src From 84eac10e04cd8d33ac0ef2ff040a77ce80850f16 Mon Sep 17 00:00:00 2001 From: king5327 Date: Wed, 26 Apr 2023 19:03:30 -0400 Subject: [PATCH 2/6] Update misc.dm --- code/_global_vars/misc.dm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/_global_vars/misc.dm b/code/_global_vars/misc.dm index c93128c3c28..297c1b33076 100644 --- a/code/_global_vars/misc.dm +++ b/code/_global_vars/misc.dm @@ -33,3 +33,6 @@ GLOBAL_PROTECT(log_directory) // TGUI MOVE ME // GLOBAL_DATUM(changelog_tgui, /datum/changelog) + +// TGS // +GLOBAL_REAL_VAR(client_count) From a07c7e3caf9e005a331ff310843f1b84bdc44c2d Mon Sep 17 00:00:00 2001 From: king5327 Date: Wed, 26 Apr 2023 19:31:59 -0400 Subject: [PATCH 3/6] Update world.dm --- code/game/world.dm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/game/world.dm b/code/game/world.dm index 809185f2a66..0491d945ccb 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -12,6 +12,8 @@ var/global/datum/global_init/init = new () Pre-map initialization stuff should go here. */ +#define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return + /datum/global_init/New() generate_gameid() load_configuration() From d879269a7e04785454d1134f0968b99b373223ab Mon Sep 17 00:00:00 2001 From: k5 Date: Sat, 29 Apr 2023 01:46:38 -0400 Subject: [PATCH 4/6] Updates TGS, adds temporary restart announcer. --- code/controllers/master.dm | 2 + tgs_dmapi/tgs.dm | 119 +++++++++++- tgs_dmapi/tgs/core/datum.dm | 2 + tgs_dmapi/tgs/includes.dm | 4 + tgs_dmapi/tgs/v3210/api.dm | 9 +- tgs_dmapi/tgs/v3210/commands.dm | 10 +- tgs_dmapi/tgs/v4/api.dm | 15 +- tgs_dmapi/tgs/v4/commands.dm | 11 +- tgs_dmapi/tgs/v5/README.md | 5 + tgs_dmapi/tgs/v5/__interop_version.dm | 1 + tgs_dmapi/tgs/v5/_defines.dm | 18 +- tgs_dmapi/tgs/v5/api.dm | 195 ++++--------------- tgs_dmapi/tgs/v5/bridge.dm | 95 ++++++++++ tgs_dmapi/tgs/v5/chunking.dm | 43 +++++ tgs_dmapi/tgs/v5/commands.dm | 38 +++- tgs_dmapi/tgs/v5/serializers.dm | 59 ++++++ tgs_dmapi/tgs/v5/topic.dm | 258 ++++++++++++++++++++++++++ tgs_dmapi/tgs/v5/undefs.dm | 16 +- 18 files changed, 703 insertions(+), 197 deletions(-) create mode 100644 tgs_dmapi/tgs/v5/__interop_version.dm create mode 100644 tgs_dmapi/tgs/v5/bridge.dm create mode 100644 tgs_dmapi/tgs/v5/chunking.dm create mode 100644 tgs_dmapi/tgs/v5/serializers.dm create mode 100644 tgs_dmapi/tgs/v5/topic.dm diff --git a/code/controllers/master.dm b/code/controllers/master.dm index b895ad2b6ce..0278f8cb2f5 100755 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -228,6 +228,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new world.TgsInitializationComplete() + world.TgsTargetedChatBroadcast(new /datum/tgs_message_content{text = "A new round has begun!"}) + // Sort subsystems by display setting for easy access. sortTim(subsystems, /proc/cmp_subsystem_display) // Set world options. diff --git a/tgs_dmapi/tgs.dm b/tgs_dmapi/tgs.dm index c4e9ec0c09f..557c1578612 100644 --- a/tgs_dmapi/tgs.dm +++ b/tgs_dmapi/tgs.dm @@ -227,6 +227,8 @@ 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 @@ -256,9 +258,11 @@ 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 string to respond to the issuer with. + * 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]. @@ -266,6 +270,107 @@ /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. @@ -296,19 +401,19 @@ /** * Send a message to connected chats. * - * message - The string to send. + * 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(message, admin_only = FALSE) +/world/proc/TgsTargetedChatBroadcast(datum/tgs_message_content/message, admin_only = FALSE) return /** * Send a private message to a specific user. * - * message - The string to send. + * message - The [/datum/tgs_message_content] to send. * user: The [/datum/tgs_chat_user] to PM. */ -/world/proc/TgsChatPrivateMessage(message, datum/tgs_chat_user/user) +/world/proc/TgsChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user) return // The following functions will sleep if a call to TgsNew() is sleeping @@ -316,10 +421,10 @@ /** * Send a message to connected chats that are flagged as game-related in TGS. * - * message - The string to send. + * 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(message, list/channels = null) +/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. diff --git a/tgs_dmapi/tgs/core/datum.dm b/tgs_dmapi/tgs/core/datum.dm index 4d37ed662d1..68b0330fe86 100644 --- a/tgs_dmapi/tgs/core/datum.dm +++ b/tgs_dmapi/tgs/core/datum.dm @@ -4,6 +4,8 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null) 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 diff --git a/tgs_dmapi/tgs/includes.dm b/tgs_dmapi/tgs/includes.dm index 4018074f4e3..23b714f9d06 100644 --- a/tgs_dmapi/tgs/includes.dm +++ b/tgs_dmapi/tgs/includes.dm @@ -13,5 +13,9 @@ #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/tgs_dmapi/tgs/v3210/api.dm b/tgs_dmapi/tgs/v3210/api.dm index 3c218d5b10b..b881662d71c 100644 --- a/tgs_dmapi/tgs/v3210/api.dm +++ b/tgs_dmapi/tgs/v3210/api.dm @@ -193,16 +193,19 @@ /datum/tgs_api/v3210/ChatChannelInfo() return list() // :omegalul: -/datum/tgs_api/v3210/ChatBroadcast(message, list/channels) +/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(message, admin_only) - ExportService("[admin_only ? SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE : SERVICE_REQUEST_IRC_BROADCAST] [message]") +/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() diff --git a/tgs_dmapi/tgs/v3210/commands.dm b/tgs_dmapi/tgs/v3210/commands.dm index 4ccfc1a8a60..d9bd287465b 100644 --- a/tgs_dmapi/tgs/v3210/commands.dm +++ b/tgs_dmapi/tgs/v3210/commands.dm @@ -10,9 +10,12 @@ 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!") + 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]) @@ -49,4 +52,7 @@ sender = "<@[sender]>" user.mention = sender - return stc.Run(user, params) || TRUE + var/datum/tgs_message_content/result = stc.Run(user, params) + result = UpgradeDeprecatedCommandResponse(result, command) + + return result?.text || TRUE diff --git a/tgs_dmapi/tgs/v4/api.dm b/tgs_dmapi/tgs/v4/api.dm index 0e37a7aa24c..2f05c386338 100644 --- a/tgs_dmapi/tgs/v4/api.dm +++ b/tgs_dmapi/tgs/v4/api.dm @@ -256,33 +256,36 @@ /datum/tgs_api/v4/Revision() return cached_revision -/datum/tgs_api/v4/ChatBroadcast(message, list/channels) +/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 = list("message" = message, "channelIds" = ids) + message = UpgradeDeprecatedChatMessage(message) + message = list("message" = message.text, "channelIds" = ids) if(intercepted_message_queue) intercepted_message_queue += list(message) else Export(TGS4_COMM_CHAT, message) -/datum/tgs_api/v4/ChatTargetedBroadcast(message, admin_only) +/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 = list("message" = message, "channelIds" = channels) + message = UpgradeDeprecatedChatMessage(message) + message = list("message" = message.text, "channelIds" = channels) if(intercepted_message_queue) intercepted_message_queue += list(message) else Export(TGS4_COMM_CHAT, message) -/datum/tgs_api/v4/ChatPrivateMessage(message, datum/tgs_chat_user/user) - message = list("message" = message, "channelIds" = list(user.channel.id)) +/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 diff --git a/tgs_dmapi/tgs/v4/commands.dm b/tgs_dmapi/tgs/v4/commands.dm index 4ca1500167b..d6d3d718d47 100644 --- a/tgs_dmapi/tgs/v4/commands.dm +++ b/tgs_dmapi/tgs/v4/commands.dm @@ -3,6 +3,9 @@ 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!") @@ -34,8 +37,8 @@ var/datum/tgs_chat_command/sc = custom_commands[command] if(sc) - var/result = sc.Run(u, params) - if(result == null) - result = "" - return result + var/datum/tgs_message_content/result = sc.Run(u, params) + result = UpgradeDeprecatedCommandResponse(result, command) + + return result?.text return "Unknown command: [command]!" diff --git a/tgs_dmapi/tgs/v5/README.md b/tgs_dmapi/tgs/v5/README.md index 5b48d57a1f0..a8a0c748e7b 100644 --- a/tgs_dmapi/tgs/v5/README.md +++ b/tgs_dmapi/tgs/v5/README.md @@ -2,7 +2,12 @@ This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no security restrictions. +- [__interop_version.dm](./__interop_version.dm) contains the version of the API used between the DMAPI and TGS. - [_defines.dm](./_defines.dm) contains constant definitions. - [api.dm](./api.dm) contains the bulk of the API code. +- [bridge.dm](./bridge.dm) contains functions related to making bridge requests. +- [chunking.dm](./chunking.dm) contains common function for splitting large raw data sets into chunks BYOND can natively process. - [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s. +- [serializers.dm](./serializers.dm) contains function to help convert interop `/datum`s into a JSON encodable `list()` format. +- [topic.dm](./topic.dm) contains functions related to processing topic requests. - [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`. diff --git a/tgs_dmapi/tgs/v5/__interop_version.dm b/tgs_dmapi/tgs/v5/__interop_version.dm new file mode 100644 index 00000000000..6ef7c86ef75 --- /dev/null +++ b/tgs_dmapi/tgs/v5/__interop_version.dm @@ -0,0 +1 @@ +"5.6.0" diff --git a/tgs_dmapi/tgs/v5/_defines.dm b/tgs_dmapi/tgs/v5/_defines.dm index 10bc4cbe406..a3f949081f1 100644 --- a/tgs_dmapi/tgs/v5/_defines.dm +++ b/tgs_dmapi/tgs/v5/_defines.dm @@ -4,16 +4,29 @@ #define DMAPI5_BRIDGE_DATA "data" #define DMAPI5_TOPIC_DATA "tgs_data" +#define DMAPI5_BRIDGE_REQUEST_LIMIT 8198 +#define DMAPI5_TOPIC_REQUEST_LIMIT 65529 +#define DMAPI5_TOPIC_RESPONSE_LIMIT 65528 + #define DMAPI5_BRIDGE_COMMAND_PORT_UPDATE 0 #define DMAPI5_BRIDGE_COMMAND_STARTUP 1 #define DMAPI5_BRIDGE_COMMAND_PRIME 2 #define DMAPI5_BRIDGE_COMMAND_REBOOT 3 #define DMAPI5_BRIDGE_COMMAND_KILL 4 #define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5 +#define DMAPI5_BRIDGE_COMMAND_CHUNK 6 #define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" #define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" +#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" @@ -25,7 +38,6 @@ #define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort" #define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation" -#define DMAPI5_CHAT_MESSAGE_TEXT "text" #define DMAPI5_CHAT_MESSAGE_CHANNEL_IDS "channelIds" #define DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER "accessIdentifier" @@ -65,6 +77,8 @@ #define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6 #define DMAPI5_TOPIC_COMMAND_HEARTBEAT 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_PARAMETER_COMMAND_TYPE "commandType" #define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand" @@ -75,6 +89,7 @@ #define DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE "chatUpdate" #define DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION "newServerVersion" +#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE "commandResponse" #define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE "commandResponseMessage" #define DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES "chatResponses" @@ -93,6 +108,7 @@ #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" diff --git a/tgs_dmapi/tgs/v5/api.dm b/tgs_dmapi/tgs/v5/api.dm index 572944ed401..517240f12f8 100644 --- a/tgs_dmapi/tgs/v5/api.dm +++ b/tgs_dmapi/tgs/v5/api.dm @@ -17,9 +17,14 @@ var/initialized = FALSE + var/chunked_requests = 0 + var/list/chunked_topics = list() + + var/detached = FALSE + /datum/tgs_api/v5/ApiVersion() return new /datum/tgs_version( - #include "interop_version.dm" + #include "__interop_version.dm" ) /datum/tgs_api/v5/OnWorldNew(minimum_required_security_level) @@ -97,172 +102,18 @@ /datum/tgs_api/v5/OnInitializationComplete() Bridge(DMAPI5_BRIDGE_COMMAND_PRIME) -/datum/tgs_api/v5/proc/TopicResponse(error_message = null) - var/list/response = list() - response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message - - return json_encode(response) - /datum/tgs_api/v5/OnTopic(T) + RequireInitialBridgeResponse() var/list/params = params2list(T) var/json = params[DMAPI5_TOPIC_DATA] if(!json) return FALSE // continue to /world/Topic - var/list/topic_parameters = json_decode(json) - if(!topic_parameters) - return TopicResponse("Invalid topic parameters json!"); - if(!initialized) - TGS_WARNING_LOG("Missed topic due to not being initialized: [T]") + TGS_WARNING_LOG("Missed topic due to not being initialized: [json]") return TRUE // too early to handle, but it's still our responsibility - var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] - if(their_sCK != access_identifier) - return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!"); - - var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] - if(!isnum(command)) - return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] from: [json]!") - - switch(command) - if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND) - var/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) - if(!result) - result = TopicResponse("Error running chat command!") - return result - if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION) - intercepted_message_queue = list() - var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION] - if(!istype(event_notification)) - return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!") - - var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE] - if(!isnum(event_type)) - return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!") - - var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS] - if(event_parameters && !istype(event_parameters)) - return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!") - - var/list/event_call = list(event_type) - if(event_parameters) - event_call += event_parameters - - if(event_handler != null) - event_handler.HandleEvent(arglist(event_call)) - - var/list/response = list() - response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue - intercepted_message_queue = null - return json_encode(response) - if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) - var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] - if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) - - //the topic still completes, miraculously - //I honestly didn't believe byond could do it without exploding - if(!world.OpenPort(new_port)) - return TopicResponse("Port change failed!") - - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE) - var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE] - if(!isnum(new_reboot_mode)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) - - reboot_mode = new_reboot_mode - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED) - var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME] - if(!istext(new_instance_name)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name) - - instance_name = new_instance_name - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE) - var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE] - if(!istype(chat_update_json)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!") - - DecodeChannels(chat_update_json) - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) - var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] - if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") - - server_port = new_port - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_HEARTBEAT) - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH) - var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] - var/error_message = null - if (new_port != null) - if (!isnum(new_port) || !(new_port > 0)) - error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]" - else - server_port = new_port - - var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION] - if (!istext(new_version_string)) - if(error_message != null) - error_message += ", " - error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]" - else - var/datum/tgs_version/new_version = new(new_version_string) - if (event_handler) - event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version) - - version = new_version - - return json_encode(list(DMAPI5_RESPONSE_ERROR_MESSAGE = error_message, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) - - return TopicResponse("Unknown command: [command]") - -/datum/tgs_api/v5/proc/Bridge(command, list/data) - if(!data) - data = list() - - data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command - data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier - - var/json = json_encode(data) - var/encoded_json = url_encode(json) - - // This is an infinite sleep until we get a response - var/export_response = world.Export("http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]") - if(!export_response) - TGS_ERROR_LOG("Failed export request: [json]") - return - - var/response_json = file2text(export_response["CONTENT"]) - if(!response_json) - TGS_ERROR_LOG("Failed export request, missing content!") - return - - var/list/bridge_response = json_decode(response_json) - if(!bridge_response) - TGS_ERROR_LOG("Failed export request, bad json: [response_json]") - return - - var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE] - if(error) - TGS_ERROR_LOG("Failed export request, bad request: [error]") - return - - return bridge_response + return ProcessTopicJson(json, TRUE) /datum/tgs_api/v5/OnReboot() var/list/result = Bridge(DMAPI5_BRIDGE_COMMAND_REBOOT) @@ -297,7 +148,15 @@ RequireInitialBridgeResponse() return revision -/datum/tgs_api/v5/ChatBroadcast(message, list/channels) +// Common proc b/c it's used by the V3/V4 APIs +/datum/tgs_api/proc/UpgradeDeprecatedChatMessage(datum/tgs_message_content/message) + if(!istext(message)) + return message + + TGS_WARNING_LOG("Received legacy string when a [/datum/tgs_message_content] was expected. Please audit all calls to TgsChatBroadcast, TgsChatTargetedBroadcast, and TgsChatPrivateMessage to ensure they use the new /datum.") + return new /datum/tgs_message_content(message) + +/datum/tgs_api/v5/ChatBroadcast(datum/tgs_message_content/message, list/channels) if(!length(channels)) channels = ChatChannelInfo() @@ -306,26 +165,33 @@ var/datum/tgs_chat_channel/channel = I ids += channel.id - message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = ids) + message = UpgradeDeprecatedChatMessage(message) + message = message._interop_serialize() + message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = ids if(intercepted_message_queue) intercepted_message_queue += list(message) else Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message)) -/datum/tgs_api/v5/ChatTargetedBroadcast(message, admin_only) +/datum/tgs_api/v5/ChatTargetedBroadcast(datum/tgs_message_content/message, admin_only) var/list/channels = list() for(var/I in ChatChannelInfo()) var/datum/tgs_chat_channel/channel = I if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only))) channels += channel.id - message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = channels) + + message = UpgradeDeprecatedChatMessage(message) + message = message._interop_serialize() + message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = channels if(intercepted_message_queue) intercepted_message_queue += list(message) else Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message)) -/datum/tgs_api/v5/ChatPrivateMessage(message, datum/tgs_chat_user/user) - message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = list(user.channel.id)) +/datum/tgs_api/v5/ChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user) + message = UpgradeDeprecatedChatMessage(message) + message = message._interop_serialize() + message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = list(user.channel.id) if(intercepted_message_queue) intercepted_message_queue += list(message) else @@ -354,6 +220,7 @@ 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() diff --git a/tgs_dmapi/tgs/v5/bridge.dm b/tgs_dmapi/tgs/v5/bridge.dm new file mode 100644 index 00000000000..b3cf7759397 --- /dev/null +++ b/tgs_dmapi/tgs/v5/bridge.dm @@ -0,0 +1,95 @@ +/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/url = "http://127.0.0.1:[server_port]/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/PerformBridgeRequest(bridge_request) + if(detached) + // Wait up to one minute + for(var/i in 1 to 600) + sleep(1) + if(!detached) + break + + // dad went out for milk cigarettes 20 years ago... + if(i == 600) + detached = 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/response_json = file2text(export_response["CONTENT"]) + if(!response_json) + TGS_ERROR_LOG("Failed bridge request, missing 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/tgs_dmapi/tgs/v5/chunking.dm b/tgs_dmapi/tgs/v5/chunking.dm new file mode 100644 index 00000000000..cd5944d34fb --- /dev/null +++ b/tgs_dmapi/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/tgs_dmapi/tgs/v5/commands.dm b/tgs_dmapi/tgs/v5/commands.dm index 6d31dd3422d..a832c81f172 100644 --- a/tgs_dmapi/tgs/v5/commands.dm +++ b/tgs_dmapi/tgs/v5/commands.dm @@ -3,14 +3,17 @@ 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_WARNING_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!") + 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_WARNING_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!") + 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 @@ -30,11 +33,28 @@ var/datum/tgs_chat_command/sc = custom_commands[command] if(sc) - var/text_response = sc.Run(u, params) - var/list/topic_response = list() - if(!istext(text_response)) - TGS_ERROR_LOG("Custom command [command] should return a string! Got: \"[text_response]\"") - text_response = null - topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = text_response - return json_encode(topic_response) + 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?.text + topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE] = response?._interop_serialize() + 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/tgs_dmapi/tgs/v5/serializers.dm b/tgs_dmapi/tgs/v5/serializers.dm new file mode 100644 index 00000000000..7f9bc731b79 --- /dev/null +++ b/tgs_dmapi/tgs/v5/serializers.dm @@ -0,0 +1,59 @@ +/datum/tgs_message_content/proc/_interop_serialize() + return list("text" = text, "embed" = embed?._interop_serialize()) + +/datum/tgs_chat_embed/proc/_interop_serialize() + CRASH("Base /proc/interop_serialize called on [type]!") + +/datum/tgs_chat_embed/structure/_interop_serialize() + var/list/serialized_fields + if(islist(fields)) + serialized_fields = list() + for(var/datum/tgs_chat_embed/field/field as anything in fields) + serialized_fields += list(field._interop_serialize()) + return list( + "title" = title, + "description" = description, + "url" = url, + "timestamp" = timestamp, + "colour" = colour, + "image" = image?._interop_serialize(), + "thumbnail" = thumbnail?._interop_serialize(), + "video" = video?._interop_serialize(), + "footer" = footer?._interop_serialize(), + "provider" = provider?._interop_serialize(), + "author" = author?._interop_serialize(), + "fields" = serialized_fields + ) + +/datum/tgs_chat_embed/media/_interop_serialize() + return list( + "url" = url, + "width" = width, + "height" = height, + "proxyUrl" = proxy_url + ) + +/datum/tgs_chat_embed/provider/_interop_serialize() + return list( + "url" = url, + "name" = name + ) + +/datum/tgs_chat_embed/provider/author/_interop_serialize() + . = ..() + .["iconUrl"] = icon_url + .["proxyIconUrl"] = proxy_icon_url + +/datum/tgs_chat_embed/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/tgs_dmapi/tgs/v5/topic.dm b/tgs_dmapi/tgs/v5/topic.dm new file mode 100644 index 00000000000..28fcc14aef8 --- /dev/null +++ b/tgs_dmapi/tgs/v5/topic.dm @@ -0,0 +1,258 @@ +/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) + 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) + var/list/topic_parameters = json_decode(json) + if(!topic_parameters) + return TopicResponse("Invalid topic parameters json: [json]!"); + + var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] + if(check_access_identifier && their_sCK != access_identifier) + return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER]!") + + var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] + if(!isnum(command)) + 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) + 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) + intercepted_message_queue = list() + var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION] + if(!istype(event_notification)) + return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!") + + var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE] + if(!isnum(event_type)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!") + + var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS] + if(event_parameters && !istype(event_parameters)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!") + + var/list/event_call = list(event_type) + if (event_type == TGS_EVENT_WATCHDOG_DETACH) + detached = TRUE + + if(event_parameters) + event_call += event_parameters + + if(event_handler != null) + event_handler.HandleEvent(arglist(event_call)) + + var/list/response = TopicResponse() + response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue + intercepted_message_queue = null + return response + + if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + if (!isnum(new_port) || !(new_port > 0)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) + + //the topic still completes, miraculously + //I honestly didn't believe byond could do it without exploding + if(!world.OpenPort(new_port)) + return TopicResponse("Port change failed!") + + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE) + var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE] + if(!isnum(new_reboot_mode)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) + + reboot_mode = new_reboot_mode + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED) + var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME] + if(!istext(new_instance_name)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name) + + instance_name = new_instance_name + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE) + var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE] + if(!istype(chat_update_json)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!") + + DecodeChannels(chat_update_json) + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + if (!isnum(new_port) || !(new_port > 0)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + + server_port = new_port + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_HEARTBEAT) + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH) + 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() + 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 + + return TopicResponse("Unknown command: [command]") diff --git a/tgs_dmapi/tgs/v5/undefs.dm b/tgs_dmapi/tgs/v5/undefs.dm index 5885a60e75c..e3455b69d1c 100644 --- a/tgs_dmapi/tgs/v5/undefs.dm +++ b/tgs_dmapi/tgs/v5/undefs.dm @@ -4,16 +4,29 @@ #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_PORT_UPDATE #undef DMAPI5_BRIDGE_COMMAND_STARTUP #undef DMAPI5_BRIDGE_COMMAND_PRIME #undef DMAPI5_BRIDGE_COMMAND_REBOOT #undef DMAPI5_BRIDGE_COMMAND_KILL #undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND +#undef DMAPI5_BRIDGE_COMMAND_CHUNK #undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER #undef DMAPI5_PARAMETER_CUSTOM_COMMANDS +#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 @@ -25,7 +38,6 @@ #undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT #undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION -#undef DMAPI5_CHAT_MESSAGE_TEXT #undef DMAPI5_CHAT_MESSAGE_CHANNEL_IDS #undef DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER @@ -75,6 +87,7 @@ #undef DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE #undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION +#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE #undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE #undef DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES @@ -93,6 +106,7 @@ #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 From b4fdbf3e6a5b8d8811c015567e6a03e7c938937a Mon Sep 17 00:00:00 2001 From: k5 Date: Sat, 29 Apr 2023 02:45:27 -0400 Subject: [PATCH 5/6] Oops. --- code/controllers/master.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/controllers/master.dm b/code/controllers/master.dm index 0278f8cb2f5..88b7c8a52f6 100755 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -228,7 +228,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new world.TgsInitializationComplete() - world.TgsTargetedChatBroadcast(new /datum/tgs_message_content{text = "A new round has begun!"}) + world.TgsTargetedChatBroadcast(new /datum/tgs_message_content{text = "A new round has begun!"}()) // Sort subsystems by display setting for easy access. sortTim(subsystems, /proc/cmp_subsystem_display) From d75a990091a8823969244a9faf56456654c9074a Mon Sep 17 00:00:00 2001 From: k5 Date: Sun, 30 Apr 2023 00:13:55 -0400 Subject: [PATCH 6/6] Double oops. --- code/controllers/master.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/controllers/master.dm b/code/controllers/master.dm index 88b7c8a52f6..f3daac1390f 100755 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -228,7 +228,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new world.TgsInitializationComplete() - world.TgsTargetedChatBroadcast(new /datum/tgs_message_content{text = "A new round has begun!"}()) + world.TgsTargetedChatBroadcast(new /datum/tgs_message_content(text = "A new round has begun!")) // Sort subsystems by display setting for easy access. sortTim(subsystems, /proc/cmp_subsystem_display)