Merge pull request #323 from Sandstorm-Station/tgs-dmapi-update

Automatic TGS DMAPI Update
This commit is contained in:
SandPoot
2023-10-10 17:42:09 -03:00
committed by GitHub
21 changed files with 843 additions and 268 deletions
+177 -61
View File
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
#define TGS_DMAPI_VERSION "6.0.6"
#define TGS_DMAPI_VERSION "6.5.3"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
@@ -12,8 +12,8 @@
// 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()()
// 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):
@@ -52,44 +52,46 @@
// EVENT CODES
/// Before a reboot mode change, extras parameters are the current and new reboot mode enums
/// 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
/// 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
/// 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
/// 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
/// 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
/// 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
/// 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
/// 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
/// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path.
#define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4
/// Before a BYOND install operation begins. Parameters: [/datum/tgs_version] of the installing BYOND
/// Before a BYOND install operation begins. Parameters: [/datum/tgs_version] of the installing BYOND.
#define TGS_EVENT_BYOND_INSTALL_START 5
/// When a BYOND install operation fails. Parameters: Error message
#define TGS_EVENT_BYOND_INSTALL_FAIL 6
/// When the active BYOND version changes. Parameters: (Nullable) [/datum/tgs_version] of the current BYOND, [/datum/tgs_version] of the new BYOND
/// When the active BYOND version changes. Parameters: (Nullable) [/datum/tgs_version] of the current BYOND, [/datum/tgs_version] of the new BYOND.
#define TGS_EVENT_BYOND_ACTIVE_VERSION_CHANGE 7
/// When the compiler starts running. Parameters: Game directory path, origin commit SHA
/// 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
/// 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
/// 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
/// 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
/// 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
/// 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
/// 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
@@ -104,6 +106,12 @@
#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, byond version.
#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
@@ -146,7 +154,7 @@
#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].
* Call this as late as possible in [world/proc/Reboot] (BEFORE ..()).
*/
/world/proc/TgsReboot()
return
@@ -158,28 +166,28 @@
/datum/tgs_revision_information
/// Full SHA of the commit.
var/commit
/// ISO 8601 timestamp of when the commit was created
/// 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
/// 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
// 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
/// The patch version number. null for wildcards.
var/patch
/// Legacy version number. Generally null
/// Legacy version number. Generally null.
var/deprecated_patch
/// Unparsed string value
/// Unparsed string value.
var/raw_parameter
/// String value minus prefix
/// String value minus prefix.
var/deprefixed_parameter
/**
@@ -225,40 +233,49 @@
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
/// 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.
/// The user's display name.
var/friendly_name
// The string to use to ping this user in a message.
/// The string to use to ping this user in a message.
var/mention
/// The [/datum/tgs_chat_channel] the user was from
/// 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
* 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
/// 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 ...`
/// 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
/// The help text displayed for this command.
var/help_text = ""
/// If this command should be available to game administrators only
/// 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 +283,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.
@@ -285,75 +403,73 @@
// 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.
* 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
* 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.
* Send a message to connected chats. This function may sleep!
*
* 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.
* Send a private message to a specific user. This function may sleep!
*
* 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
/**
* Send a message to connected chats that are flagged as game-related in TGS.
* Send a message to connected chats that are flagged as game-related in TGS. This function may sleep!
*
* 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.
/// 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 current [/datum/tgs_version] of the DMAPI being used if it was activated, null otherwise.
/// 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.
/// 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.
/// 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.
/// 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 a list of active [/datum/tgs_revision_information/test_merge]s if TGS is present, null otherwise.
/// 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.
/// 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 Jordan Brown
Copyright (c) 2017-2023 Jordan Brown
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License
Copyright (c) 2017 Jordan Brown
Copyright (c) 2017-2023 Jordan Brown
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
+2 -1
View File
@@ -5,4 +5,5 @@ This folder contains all DMAPI code not directly involved in an API.
- [_definitions.dm](./definitions.dm) contains defines needed across DMAPI internals.
- [core.dm](./core.dm) contains the implementations of the `/world/proc/TgsXXX()` procs. Many map directly to the `/datum/tgs_api` functions. It also contains the /datum selection and setup code.
- [datum.dm](./datum.dm) contains the `/datum/tgs_api` declarations that all APIs must implement.
- [tgs_version.dm](./tgs_version.dm) contains the `/datum/tgs_version` definition
- [tgs_version.dm](./tgs_version.dm) contains the `/datum/tgs_version` definition
-
+8
View File
@@ -1,2 +1,10 @@
#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
+2
View File
@@ -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
+4
View File
@@ -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"
+7 -4
View File
@@ -179,7 +179,7 @@
/datum/tgs_api/v3210/Revision()
if(!warned_revison)
var/datum/tgs_version/api_version = ApiVersion()
TGS_ERROR_LOG("Use of TgsRevision on [api_version.deprefixed_parameter] origin_commit only points to master!")
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
@@ -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()
+9 -3
View File
@@ -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])
@@ -44,9 +47,12 @@
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]+$")
var/regex/discord_id_regex = regex("^\[0-9\]+$")
if(findtext(sender, discord_id_regex))
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 ? result.text : TRUE
+19 -6
View File
@@ -256,33 +256,46 @@
/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)
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(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)
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(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
+7 -4
View File
@@ -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 ? result.text : TRUE
return "Unknown command: [command]!"
+5
View File
@@ -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`.
+1
View File
@@ -0,0 +1 @@
"5.6.1"
+18 -2
View File
@@ -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 65528
#define DMAPI5_TOPIC_RESPONSE_LIMIT 65529
#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"
@@ -63,8 +75,10 @@
#define DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED 4
#define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 5
#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6
#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7
#define DMAPI5_TOPIC_COMMAND_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_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"
+67 -174
View File
@@ -17,12 +17,22 @@
var/initialized = FALSE
var/chunked_requests = 0
var/list/chunked_topics = list()
var/detached = FALSE
/datum/tgs_api/v5/New()
. = ..()
TGS_DEBUG_LOG("V5 API created")
/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)
TGS_DEBUG_LOG("OnWorldNew()")
server_port = world.params[DMAPI5_PARAM_SERVER_PORT]
access_identifier = world.params[DMAPI5_PARAM_ACCESS_IDENTIFIER]
@@ -91,178 +101,35 @@
return TRUE
/datum/tgs_api/v5/proc/RequireInitialBridgeResponse()
TGS_DEBUG_LOG("RequireInitialBridgeResponse()")
var/logged = FALSE
while(!version)
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/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)
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
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 +164,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/message2, list/channels)
if(!length(channels))
channels = ChatChannelInfo()
@@ -306,36 +181,53 @@
var/datum/tgs_chat_channel/channel = I
ids += channel.id
message = list(DMAPI5_CHAT_MESSAGE_TEXT = 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))
message2 = UpgradeDeprecatedChatMessage(message2)
/datum/tgs_api/v5/ChatTargetedBroadcast(message, admin_only)
if (!length(channels))
return
var/list/data = message2._interop_serialize()
data[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = ids
if(intercepted_message_queue)
intercepted_message_queue += list(data)
else
Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = data))
/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
message = list(DMAPI5_CHAT_MESSAGE_TEXT = 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))
message2 = UpgradeDeprecatedChatMessage(message2)
if (!length(channels))
return
var/list/data = message2._interop_serialize()
data[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = channels
if(intercepted_message_queue)
intercepted_message_queue += list(message)
intercepted_message_queue += list(data)
else
Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message))
Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = data))
/datum/tgs_api/v5/ChatPrivateMessage(datum/tgs_message_content/message2, datum/tgs_chat_user/user)
message2 = UpgradeDeprecatedChatMessage(message2)
var/list/data = message2._interop_serialize()
data[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = list(user.channel.id)
if(intercepted_message_queue)
intercepted_message_queue += list(data)
else
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()
@@ -354,6 +246,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()
+99
View File
@@ -0,0 +1,99 @@
/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/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/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
+43
View File
@@ -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
+29 -9
View File
@@ -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 ? 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
-1
View File
@@ -1 +0,0 @@
"5.3.0"
+59
View File
@@ -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
)
+270
View File
@@ -0,0 +1,270 @@
/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()
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]")
+16 -2
View File
@@ -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
@@ -63,7 +75,7 @@
#undef DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED
#undef DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE
#undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE
#undef DMAPI5_TOPIC_COMMAND_HEARTBEAT
#undef DMAPI5_TOPIC_COMMAND_HEALTHCHECK
#undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH
#undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE
@@ -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