From c5ba21994d34e10a73ca482bdd558395d069c03d Mon Sep 17 00:00:00 2001 From: SandPoot Date: Sun, 8 Oct 2023 12:49:40 -0300 Subject: [PATCH 1/6] If i had a nickel for every time incapacitated() had some issue, i would have... --- code/modules/mob/living/carbon/human/human.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 144992c04f..b8d102a2f1 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -816,7 +816,7 @@ /mob/living/carbon/human/proc/piggyback(mob/living/carbon/target) if(can_piggyback(target)) visible_message("[target] starts to climb onto [src]...") - if(do_after(target, 1.5 SECONDS, src, extra_checks = CALLBACK(src, PROC_REF(can_piggyback), target))) + if(do_after(target, 1.5 SECONDS, src, IGNORE_INCAPACITATED, extra_checks = CALLBACK(src, PROC_REF(can_piggyback), target))) if(can_piggyback(target)) if(target.incapacitated(FALSE, TRUE) || incapacitated(FALSE, TRUE)) target.visible_message("[target] can't hang onto [src]!") From b624de1c101e22b15c75ea20bdcc7f94bc45a48c Mon Sep 17 00:00:00 2001 From: SandPoot Date: Sun, 8 Oct 2023 13:01:15 -0300 Subject: [PATCH 2/6] you go --- code/modules/mob/dead/observer/respawn.dm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/modules/mob/dead/observer/respawn.dm b/code/modules/mob/dead/observer/respawn.dm index 32f775ed46..5d1f65311d 100644 --- a/code/modules/mob/dead/observer/respawn.dm +++ b/code/modules/mob/dead/observer/respawn.dm @@ -35,8 +35,8 @@ message_admins("[key_name_admin(src)] gave [key_name_admin(O)] a full respawn and sent them back to the lobby.") log_admin("[key_name(src)] gave [key_name(O)] a full respawn and sent them back to the lobby.") to_chat(O, "You have been given a full respawn.") - O.do_respawn(FALSE) - O.client.prefs.dnr_triggered = FALSE + if(O.do_respawn(FALSE)) + player.prefs.dnr_triggered = FALSE else if(istype(M, /mob/dead/new_player)) var/mob/dead/new_player/NP = M var/confirm = alert(src, "Remove [NP]'s respawn restrictions?", "Remove Restrictions", "Yes", "No") @@ -150,7 +150,7 @@ */ /mob/dead/observer/proc/do_respawn(penalize) if(!client) - return + return FALSE if(isnull(penalize)) penalize = client.prefs.respawn_restrictions_active client.prefs.respawn_restrictions_active = penalize @@ -162,6 +162,7 @@ to_chat(N, "You have been respawned to the lobby. \ Remember to take heed of rules regarding round knowledge - notably, that ALL past lives are forgotten. \ Any character you join as has NO knowledge of round events unless specified otherwise by an admin.") + return TRUE /** * Actual proc that removes us and puts us back on lobby From a5515e0cf8fc960e9f7d2eca53567f1e569eb637 Mon Sep 17 00:00:00 2001 From: SandPoot Date: Sun, 8 Oct 2023 19:04:06 -0300 Subject: [PATCH 3/6] push --- .../tgui/interfaces/MobInteraction.tsx | 1014 +++++++++-------- 1 file changed, 528 insertions(+), 486 deletions(-) diff --git a/tgui/packages/tgui/interfaces/MobInteraction.tsx b/tgui/packages/tgui/interfaces/MobInteraction.tsx index 21b1a960b5..2278a2d5bd 100644 --- a/tgui/packages/tgui/interfaces/MobInteraction.tsx +++ b/tgui/packages/tgui/interfaces/MobInteraction.tsx @@ -1,5 +1,6 @@ import { filter, map, sortBy } from 'common/collections'; import { flow } from 'common/fp'; +import { clamp } from 'common/math'; import { createSearch } from 'common/string'; import { useBackend, useLocalState } from '../backend'; import { BlockQuote, Button, Flex, Icon, Input, LabeledList, ProgressBar, Section, Table, Tabs, Stack } from '../components'; @@ -85,6 +86,10 @@ export const MobInteraction = (props, context) => { theirLust, theirMaxLust, } = data; + const [ + searchText, + setSearchText, + ] = useLocalState(context, 'searchText', ''); const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0); return ( @@ -92,46 +97,52 @@ export const MobInteraction = (props, context) => { width={430} height={700} resizable> - -
+ +
- - -
- You...
- {selfAttributes.map(attribute => ( -
- {attribute}
-
- ))} -
-
- {!isTargetSelf ? ( - +
+ +
- They...
- {theirAttributes.map(attribute => ( + You...
+ {selfAttributes.map(attribute => (
{attribute}
))}
- ) : (null)} -
- - - - - {(!isTargetSelf && !isNaN(theirLust)) ? ( - - + {!isTargetSelf ? ( + +
+ They...
+ {theirAttributes.map(attribute => ( +
+ {attribute}
+
+ ))} +
+
+ ) : (null)} +
+
+
+ + + - ) : (null)} - + {(!isTargetSelf && !isNaN(theirLust)) ? ( + + + + ) : (null)} + +
-
+
setTabIndex(0)}> Interactions @@ -146,6 +157,19 @@ export const MobInteraction = (props, context) => { Preferences + + + + + + setSearchText(value)} /> + + {tabIndex === 0 && ( ) || tabIndex === 1 && ( @@ -169,63 +193,51 @@ const InteractionsTab = (props, context) => { ] = useLocalState(context, 'searchText', ''); const interactions = sortInteractions(data.interactions, searchText) || []; return ( - - - - - - - - setSearchText(value)} /> - - -
-
- { - interactions.length ? ( - interactions.map((interaction) => ( - - - )))} - - - )) - ) : ( -
- { - searchText ? ( - "No matching results." - ) : ( - "No interactions available." - ) - } -
- ) - } -
+
+ + { + interactions.length ? ( + interactions.map((interaction) => ( + + + )))} + + + )) + ) : ( +
+ { + searchText ? ( + "No matching results." + ) : ( + "No interactions available." + ) + } +
+ ) + } +
+
); }; @@ -244,6 +256,20 @@ export const sortInteractions = (interactions, searchText = '') => { ])(interactions); }; +/** + * Genital sorter! + */ +export const sortGenitals = (genitals, searchText = '') => { + const testSearch = createSearch(searchText, + genital => genital.name); + return flow([ + // Optional search term + searchText && filter(testSearch), + // Slightly expensive, but way better than sorting in BYOND + sortBy(genital => genital.name), + ])(genitals); +}; + // Self explanatory const ModeToIcon = { "Always visible": "eye", @@ -265,65 +291,77 @@ const ModeToIcon = { */ const GenitalTab = (props, context) => { const { act, data } = useBackend(context); - const genitals = data.genitals || []; + const [ + searchText, + setSearchText, + ] = useLocalState(context, 'searchText', ''); + const genitals = sortGenitals(data.genitals, searchText) || []; return ( - genitals.length ? ( - - {genitals.map(genital => ( -
- - - Visibility
- {genital.possible_choices.map(choice => ( +
+ {genitals.length ? ( + + {genitals.map(genital => ( +
+
+ + Visibility
+ {genital.possible_choices.map(choice => ( +
-
- ))} -
- ) : ( -
- You don't seem to have any genitals... - Or any that you could modify. -
- ) +
+ ))} + + ) : ( +
+ { + searchText ? ( + "No matching results." + ) : ( + "You don't seem to have any genitals...\ + Or any that you could modify." + ) + } +
+ )} +
); }; @@ -337,120 +375,122 @@ const CharacterPrefsTab = (props, context) => { extreme_harm, } = data; return ( - - - -
); }; @@ -479,247 +519,249 @@ const ContentPreferencesTab = (props, context) => { no_auto_wag, } = data; return ( - - -
+
+ + +
+
); }; From b29219aa1adab5f94c843ad6fbef56cc284423d0 Mon Sep 17 00:00:00 2001 From: tgstation-server Date: Tue, 10 Oct 2023 01:05:15 +0000 Subject: [PATCH 4/6] Update TGS DMAPI --- code/__DEFINES/tgs.dm | 238 +++++++++++++++----- code/modules/tgs/LICENSE | 2 +- code/modules/tgs/core/README.md | 3 +- code/modules/tgs/core/_definitions.dm | 8 + code/modules/tgs/core/datum.dm | 2 + code/modules/tgs/includes.dm | 4 + code/modules/tgs/v3210/api.dm | 11 +- code/modules/tgs/v3210/commands.dm | 12 +- code/modules/tgs/v4/api.dm | 25 ++- code/modules/tgs/v4/commands.dm | 11 +- code/modules/tgs/v5/README.md | 5 + code/modules/tgs/v5/__interop_version.dm | 1 + code/modules/tgs/v5/_defines.dm | 20 +- code/modules/tgs/v5/api.dm | 241 ++++++-------------- code/modules/tgs/v5/bridge.dm | 99 +++++++++ code/modules/tgs/v5/chunking.dm | 43 ++++ code/modules/tgs/v5/commands.dm | 38 +++- code/modules/tgs/v5/interop_version.dm | 1 - code/modules/tgs/v5/serializers.dm | 59 +++++ code/modules/tgs/v5/topic.dm | 270 +++++++++++++++++++++++ code/modules/tgs/v5/undefs.dm | 18 +- 21 files changed, 843 insertions(+), 268 deletions(-) create mode 100644 code/modules/tgs/v5/__interop_version.dm create mode 100644 code/modules/tgs/v5/bridge.dm create mode 100644 code/modules/tgs/v5/chunking.dm delete mode 100644 code/modules/tgs/v5/interop_version.dm create mode 100644 code/modules/tgs/v5/serializers.dm create mode 100644 code/modules/tgs/v5/topic.dm diff --git a/code/__DEFINES/tgs.dm b/code/__DEFINES/tgs.dm index 83e06658c5..6187a67825 100644 --- a/code/__DEFINES/tgs.dm +++ b/code/__DEFINES/tgs.dm @@ -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 diff --git a/code/modules/tgs/LICENSE b/code/modules/tgs/LICENSE index 221f9e1deb..2bedf9a63a 100644 --- a/code/modules/tgs/LICENSE +++ b/code/modules/tgs/LICENSE @@ -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 diff --git a/code/modules/tgs/core/README.md b/code/modules/tgs/core/README.md index aa3c7a9c9d..b82d8f49e2 100644 --- a/code/modules/tgs/core/README.md +++ b/code/modules/tgs/core/README.md @@ -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 \ No newline at end of file +- [tgs_version.dm](./tgs_version.dm) contains the `/datum/tgs_version` definition +- diff --git a/code/modules/tgs/core/_definitions.dm b/code/modules/tgs/core/_definitions.dm index ebf6d17c2a..fd98034eb7 100644 --- a/code/modules/tgs/core/_definitions.dm +++ b/code/modules/tgs/core/_definitions.dm @@ -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 diff --git a/code/modules/tgs/core/datum.dm b/code/modules/tgs/core/datum.dm index 4d37ed662d..68b0330fe8 100644 --- a/code/modules/tgs/core/datum.dm +++ b/code/modules/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/code/modules/tgs/includes.dm b/code/modules/tgs/includes.dm index 4018074f4e..23b714f9d0 100644 --- a/code/modules/tgs/includes.dm +++ b/code/modules/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/code/modules/tgs/v3210/api.dm b/code/modules/tgs/v3210/api.dm index 3c218d5b10..666201a322 100644 --- a/code/modules/tgs/v3210/api.dm +++ b/code/modules/tgs/v3210/api.dm @@ -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() diff --git a/code/modules/tgs/v3210/commands.dm b/code/modules/tgs/v3210/commands.dm index 4ccfc1a8a6..e65c816320 100644 --- a/code/modules/tgs/v3210/commands.dm +++ b/code/modules/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]) @@ -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 diff --git a/code/modules/tgs/v4/api.dm b/code/modules/tgs/v4/api.dm index 0e37a7aa24..b9a75c4abb 100644 --- a/code/modules/tgs/v4/api.dm +++ b/code/modules/tgs/v4/api.dm @@ -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 diff --git a/code/modules/tgs/v4/commands.dm b/code/modules/tgs/v4/commands.dm index 4ca1500167..25dd6740e3 100644 --- a/code/modules/tgs/v4/commands.dm +++ b/code/modules/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 ? result.text : TRUE return "Unknown command: [command]!" diff --git a/code/modules/tgs/v5/README.md b/code/modules/tgs/v5/README.md index 5b48d57a1f..a8a0c748e7 100644 --- a/code/modules/tgs/v5/README.md +++ b/code/modules/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/code/modules/tgs/v5/__interop_version.dm b/code/modules/tgs/v5/__interop_version.dm new file mode 100644 index 0000000000..5d3d491a73 --- /dev/null +++ b/code/modules/tgs/v5/__interop_version.dm @@ -0,0 +1 @@ +"5.6.1" diff --git a/code/modules/tgs/v5/_defines.dm b/code/modules/tgs/v5/_defines.dm index 10bc4cbe40..f973338daa 100644 --- a/code/modules/tgs/v5/_defines.dm +++ b/code/modules/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 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" diff --git a/code/modules/tgs/v5/api.dm b/code/modules/tgs/v5/api.dm index 572944ed40..34cc43f876 100644 --- a/code/modules/tgs/v5/api.dm +++ b/code/modules/tgs/v5/api.dm @@ -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() diff --git a/code/modules/tgs/v5/bridge.dm b/code/modules/tgs/v5/bridge.dm new file mode 100644 index 0000000000..37f58bcdf6 --- /dev/null +++ b/code/modules/tgs/v5/bridge.dm @@ -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 diff --git a/code/modules/tgs/v5/chunking.dm b/code/modules/tgs/v5/chunking.dm new file mode 100644 index 0000000000..cd5944d34f --- /dev/null +++ b/code/modules/tgs/v5/chunking.dm @@ -0,0 +1,43 @@ +/datum/tgs_api/v5/proc/GenerateChunks(payload, bridge) + var/limit = bridge ? DMAPI5_BRIDGE_REQUEST_LIMIT : DMAPI5_TOPIC_RESPONSE_LIMIT + + var/payload_id = ++chunked_requests + var/data_length = length(payload) + + var/chunk_count + var/list/chunk_requests + for(chunk_count = 2; !chunk_requests; ++chunk_count) + var/max_chunk_size = -round(-(data_length / chunk_count)) + if(max_chunk_size > limit) + continue + + chunk_requests = list() + for(var/i in 1 to chunk_count) + var/start_index = 1 + ((i - 1) * max_chunk_size) + if (start_index > data_length) + break + + var/end_index = min(1 + (i * max_chunk_size), data_length + 1) + + var/chunk_payload = copytext(payload, start_index, end_index) + + // sequence IDs in interop chunking are always zero indexed + var/list/chunk = list(DMAPI5_CHUNK_PAYLOAD_ID = payload_id, DMAPI5_CHUNK_SEQUENCE_ID = (i - 1), DMAPI5_CHUNK_TOTAL = chunk_count, DMAPI5_CHUNK_PAYLOAD = chunk_payload) + + var/chunk_request = list(DMAPI5_CHUNK = chunk) + var/chunk_length + if(bridge) + chunk_request = CreateBridgeRequest(DMAPI5_BRIDGE_COMMAND_CHUNK, chunk_request) + chunk_length = length(chunk_request) + else + chunk_request = list(chunk_request) // wrap for adding to list + chunk_length = length(json_encode(chunk_request)) + + if(chunk_length > limit) + // Screwed by encoding, no way to preempt it though + chunk_requests = null + break + + chunk_requests += chunk_request + + return chunk_requests diff --git a/code/modules/tgs/v5/commands.dm b/code/modules/tgs/v5/commands.dm index 6d31dd3422..9557f8a08e 100644 --- a/code/modules/tgs/v5/commands.dm +++ b/code/modules/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 ? response.text : null + topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE] = response ? response._interop_serialize() : null + return topic_response return TopicResponse("Unknown custom chat command: [command]!") + +// Common proc b/c it's used by the V3/V4 APIs +/datum/tgs_api/proc/UpgradeDeprecatedCommandResponse(datum/tgs_message_content/response, command) + // Backwards compatibility, used to return a string + if(istext(response)) + warned_deprecated_command_runs = warned_deprecated_command_runs || list() + if(!warned_deprecated_command_runs[command]) + TGS_WARNING_LOG("Custom chat command \"[command]\" is still returning a string. This behaviour is deprecated, please upgrade it to return a [/datum/tgs_message_content].") + warned_deprecated_command_runs[command] = TRUE + + return new /datum/tgs_message_content(response) + + if(!istype(response)) + TGS_ERROR_LOG("Custom chat command \"[command]\" should return a [/datum/tgs_message_content]! Got: \"[response]\"") + return null + + return response diff --git a/code/modules/tgs/v5/interop_version.dm b/code/modules/tgs/v5/interop_version.dm deleted file mode 100644 index c7bf62ecae..0000000000 --- a/code/modules/tgs/v5/interop_version.dm +++ /dev/null @@ -1 +0,0 @@ -"5.3.0" diff --git a/code/modules/tgs/v5/serializers.dm b/code/modules/tgs/v5/serializers.dm new file mode 100644 index 0000000000..3a32848ad5 --- /dev/null +++ b/code/modules/tgs/v5/serializers.dm @@ -0,0 +1,59 @@ +/datum/tgs_message_content/proc/_interop_serialize() + return list("text" = text, "embed" = embed ? embed._interop_serialize() : null) + +/datum/tgs_chat_embed/proc/_interop_serialize() + CRASH("Base /proc/interop_serialize called on [type]!") + +/datum/tgs_chat_embed/structure/_interop_serialize() + var/list/serialized_fields + if(istype(fields, /list)) + serialized_fields = list() + for(var/datum/tgs_chat_embed/field/field as anything in fields) + serialized_fields += list(field._interop_serialize()) + return list( + "title" = title, + "description" = description, + "url" = url, + "timestamp" = timestamp, + "colour" = colour, + "image" = src.image ? src.image._interop_serialize() : null, + "thumbnail" = thumbnail ? thumbnail._interop_serialize() : null, + "video" = video ? video._interop_serialize() : null, + "footer" = footer ? footer._interop_serialize() : null, + "provider" = provider ? provider._interop_serialize() : null, + "author" = author ? author._interop_serialize() : null, + "fields" = serialized_fields + ) + +/datum/tgs_chat_embed/media/_interop_serialize() + return list( + "url" = url, + "width" = width, + "height" = height, + "proxyUrl" = proxy_url + ) + +/datum/tgs_chat_embed/provider/_interop_serialize() + return list( + "url" = url, + "name" = name + ) + +/datum/tgs_chat_embed/provider/author/_interop_serialize() + . = ..() + .["iconUrl"] = icon_url + .["proxyIconUrl"] = proxy_icon_url + +/datum/tgs_chat_embed/footer/_interop_serialize() + return list( + "text" = text, + "iconUrl" = icon_url, + "proxyIconUrl" = proxy_icon_url + ) + +/datum/tgs_chat_embed/field/_interop_serialize() + return list( + "name" = name, + "value" = value, + "isInline" = is_inline + ) diff --git a/code/modules/tgs/v5/topic.dm b/code/modules/tgs/v5/topic.dm new file mode 100644 index 0000000000..d7d4712138 --- /dev/null +++ b/code/modules/tgs/v5/topic.dm @@ -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]") diff --git a/code/modules/tgs/v5/undefs.dm b/code/modules/tgs/v5/undefs.dm index 5885a60e75..c679737dfc 100644 --- a/code/modules/tgs/v5/undefs.dm +++ b/code/modules/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 @@ -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 From 41671a5f22200b2a8f7a2cf1d75bca854edd209e Mon Sep 17 00:00:00 2001 From: SandPoot Date: Tue, 10 Oct 2023 18:30:48 -0300 Subject: [PATCH 5/6] quick dmapi stuff update --- code/__HELPERS/chat.dm | 18 ++++--- code/__HELPERS/roundend.dm | 2 +- code/controllers/subsystem/ticker.dm | 4 +- code/modules/admin/chat_commands.dm | 52 +++++++------------ code/modules/discord/tgs_commands.dm | 33 ++++++++++++ .../mob/living/simple_animal/parrot.dm | 2 +- .../code/modules/tgs/chat_commands.dm | 6 +-- tgstation.dme | 1 + 8 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 code/modules/discord/tgs_commands.dm diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm index 57824b6286..a45bbe23d5 100644 --- a/code/__HELPERS/chat.dm +++ b/code/__HELPERS/chat.dm @@ -18,7 +18,7 @@ For example if you have the following channels in tgs4 set up and you make the call: -send2chat("I sniff butts", CONFIG_GET(string/where_to_send_sniff_butts)) +send2chat(new /datum/tgs_message_content("I sniff butts"), CONFIG_GET(string/where_to_send_sniff_butts)) and the config option is set like: @@ -40,23 +40,22 @@ In TGS3 it will always be sent to all connected designated game chats. * * message - The message to send. * channel_tag - Required. If "", the message with be sent to all connected (Game-type for TGS3) channels. Otherwise, it will be sent to TGS4 channels with that tag (Delimited by ','s). + * admin_only - Determines if this communication can only be sent to admin only channels. */ -/proc/send2chat(message, channel_tag) +/proc/send2chat(message, channel_tag, admin_only = FALSE) if(channel_tag == null || !world.TgsAvailable()) return var/datum/tgs_version/version = world.TgsVersion() if(channel_tag == "" || version.suite == 3) - world.TgsTargetedChatBroadcast(message, FALSE) + world.TgsTargetedChatBroadcast(message, admin_only) return - var/list/channels_to_use = list() for(var/I in world.TgsChatChannelInfo()) var/datum/tgs_chat_channel/channel = I var/list/applicable_tags = splittext(channel.custom_tag, ",") - if(channel_tag in applicable_tags) + if((!admin_only || channel.is_admin_channel) && (channel_tag in applicable_tags)) channels_to_use += channel - if(channels_to_use.len) world.TgsChatBroadcast(message, channels_to_use) @@ -69,6 +68,9 @@ In TGS3 it will always be sent to all connected designated game chats. /proc/send2adminchat(category, message, embed_links = FALSE) category = replacetext(replacetext(category, "\proper", ""), "\improper", "") message = replacetext(replacetext(message, "\proper", ""), "\improper", "") - // if(!embed_links) - // message = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ") + if(!embed_links) + message = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ") world.TgsTargetedChatBroadcast("[category] | [message]", TRUE) + +/// Handles text formatting for item use hints in examine text +#define EXAMINE_HINT(text) ("" + text + "") diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 0a9e68a790..4a68f219d2 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -275,7 +275,7 @@ if(CONFIG_GET(string/chat_reboot_role)) broadcastmessage += "\n\n<@&[CONFIG_GET(string/chat_reboot_role)]>, the server will reboot shortly!" - send2chat(broadcastmessage, CONFIG_GET(string/chat_roundend_notice_tag)) + send2chat(new /datum/tgs_message_content(broadcastmessage), CONFIG_GET(string/chat_roundend_notice_tag)) CHECK_TICK diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index a67efdf178..a04fd9ef4e 100755 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -162,7 +162,7 @@ SUBSYSTEM_DEF(ticker) for(var/client/C in GLOB.clients) window_flash(C, ignorepref = TRUE) //let them know lobby has opened up. to_chat(world, "Welcome to [station_name()]!") - send2chat("New round starting on [SSmapping.config.map_name]!", CONFIG_GET(string/chat_announce_new_game)) + send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.config.map_name]!"), CONFIG_GET(string/chat_announce_new_game)) current_state = GAME_STATE_PREGAME SEND_SIGNAL(src, COMSIG_TICKER_ENTER_PREGAME) @@ -342,7 +342,7 @@ SUBSYSTEM_DEF(ticker) var/list/allmins = adm["present"] send2adminchat("Server", "Round [GLOB.round_id ? "#[GLOB.round_id]:" : "of"] [hide_mode ? "secret":"[mode.name]"] has started[allmins.len ? ".":" with no active admins online!"]") if(CONFIG_GET(string/new_round_ping)) - send2chat("<@&[CONFIG_GET(string/new_round_ping)]> | A new round has started on [SSmapping.config.map_name]!", CONFIG_GET(string/chat_announce_new_game)) + send2chat(new /datum/tgs_message_content("<@&[CONFIG_GET(string/new_round_ping)]> | A new round has started on [SSmapping.config.map_name]!"), CONFIG_GET(string/chat_announce_new_game)) setup_done = TRUE for(var/i in GLOB.start_landmarks_list) diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm index 8dfca20d4f..88e17e6093 100644 --- a/code/modules/admin/chat_commands.dm +++ b/code/modules/admin/chat_commands.dm @@ -15,22 +15,8 @@ var/list/allmins = adm["total"] var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). " status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]." - return status + return new /datum/tgs_message_content(status) -/datum/tgs_chat_command/irccheck - name = "check" - help_text = "Gets the playercount, round info, and address of the server" - var/last_irc_check = 0 - -/datum/tgs_chat_command/irccheck/Run(datum/tgs_chat_user/sender, params) - var/rtod = REALTIMEOFDAY - if(rtod - last_irc_check < IRC_STATUS_THROTTLE) - return - last_irc_check = rtod - var/server = CONFIG_GET(string/server) - return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active for [DisplayTimeText(world.time - SSticker.round_start_time, 1)]" : "Finishing") : "Starting"] -- [server ? server : "" - //CIT CHANGE obfuscates the gamemode for TGS bot commands on discord by removing Mode:[GLOB.master_mode] - //sandstorm change, says how long the round has been on for /datum/tgs_chat_command/ahelp name = "ahelp" help_text = " |list>>" @@ -39,7 +25,7 @@ /datum/tgs_chat_command/ahelp/Run(datum/tgs_chat_user/sender, params) var/list/all_params = splittext(params, " ") if(all_params.len < 2) - return "Insufficient parameters" + return new /datum/tgs_message_content("Insufficient parameters") var/target = all_params[1] all_params.Cut(1, 2) var/id = text2num(target) @@ -48,10 +34,10 @@ if(AH) target = AH.initiator_ckey else - return "Ticket #[id] not found!" + return new /datum/tgs_message_content("Ticket #[id] not found!") var/res = IrcPm(target, all_params.Join(" "), sender.friendly_name) if(res != "Message Successful") - return res + return new /datum/tgs_message_content(res) /datum/tgs_chat_command/namecheck name = "namecheck" @@ -64,7 +50,7 @@ return "Insufficient parameters" log_admin("Chat Name Check: [sender.friendly_name] on [params]") message_admins("Name checking [params] from [sender.friendly_name]") - return keywords_lookup(params, 1) + return new /datum/tgs_message_content(keywords_lookup(params, 1)) /datum/tgs_chat_command/adminwho name = "adminwho" @@ -72,7 +58,7 @@ admin_only = TRUE /datum/tgs_chat_command/adminwho/Run(datum/tgs_chat_user/sender, params) - return ircadminwho() + return new /datum/tgs_message_content(ircadminwho()) GLOBAL_LIST(round_end_notifiees) @@ -83,10 +69,10 @@ GLOBAL_LIST(round_end_notifiees) /datum/tgs_chat_command/notify/Run(datum/tgs_chat_user/sender, params) if(!SSticker.IsRoundInProgress() && SSticker.HasRoundStarted()) - return "[sender.mention], the round has already ended!" + return new /datum/tgs_message_content("[sender.mention], the round has already ended!") LAZYINITLIST(GLOB.round_end_notifiees) GLOB.round_end_notifiees[sender.mention] = TRUE - return "I will notify [sender.mention] when the round ends." + return new /datum/tgs_message_content("I will notify [sender.mention] when the round ends.") /datum/tgs_chat_command/sdql name = "sdql" @@ -95,12 +81,12 @@ GLOBAL_LIST(round_end_notifiees) /datum/tgs_chat_command/sdql/Run(datum/tgs_chat_user/sender, params) if(GLOB.AdminProcCaller) - return "Unable to run query, another admin proc call is in progress. Try again later." + return new /datum/tgs_message_content("Unable to run query, another admin proc call is in progress. Try again later.") GLOB.AdminProcCaller = "CHAT_[sender.friendly_name]" //_ won't show up in ckeys so it'll never match with a real admin var/list/results = world.SDQL2_query(params, GLOB.AdminProcCaller, GLOB.AdminProcCaller) GLOB.AdminProcCaller = null if(!results) - return "Query produced no output" + return new /datum/tgs_message_content("Query produced no output") var/list/text_res = results.Copy(1, 3) var/list/refs = results.len > 3 ? results.Copy(4) : null if(refs) @@ -112,7 +98,7 @@ GLOBAL_LIST(round_end_notifiees) else L += "[ref]" refs = L - . = "[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]" + . = new /datum/tgs_message_content("[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]") /datum/tgs_chat_command/reload_admins name = "reload_admins" @@ -122,7 +108,7 @@ GLOBAL_LIST(round_end_notifiees) /datum/tgs_chat_command/reload_admins/Run(datum/tgs_chat_user/sender, params) ReloadAsync() log_admin("[sender.friendly_name] reloaded admins via chat command.") - return "Admins reloaded." + return new /datum/tgs_message_content("Admins reloaded.") /datum/tgs_chat_command/reload_admins/proc/ReloadAsync() set waitfor = FALSE @@ -135,14 +121,14 @@ GLOBAL_LIST(round_end_notifiees) /datum/tgs_chat_command/addbunkerbypass/Run(datum/tgs_chat_user/sender, params) if(!CONFIG_GET(flag/sql_enabled)) - return "The Database is not enabled!" + return new /datum/tgs_message_content("The Database is not enabled!") GLOB.bunker_passthrough |= ckey(params) GLOB.bunker_passthrough[ckey(params)] = world.realtime SSpersistence.SavePanicBunker() //we can do this every time, it's okay log_admin("[sender.friendly_name] has added [params] to the current round's bunker bypass list.") message_admins("[sender.friendly_name] has added [params] to the current round's bunker bypass list.") - return "[params] has been added to the current round's bunker bypass list." + return new /datum/tgs_message_content("[params] has been added to the current round's bunker bypass list.") // More (silly) chat commands citadel added. /datum/tgs_chat_command/wheelofsalt @@ -159,21 +145,21 @@ GLOBAL_LIST(round_end_notifiees) saltresult += " @everyone gets some salt this time too" else saltresult += "[saltprimarysubject] [saltsecondarysubject]" - return "[saltresult]!" + return new /datum/tgs_message_content("[saltresult]!") /datum/tgs_chat_command/valentine name = "valentine" help_text = "Get a random flirt line." /datum/tgs_chat_command/valentine/Run(datum/tgs_chat_user/sender, params) - return "[pick(GLOB.flirts)]" + return new /datum/tgs_message_content("[pick(GLOB.flirts)]") /datum/tgs_chat_command/despacito name = "despacito" //someone please high effort this sometime and make it a full on ytdl search help_text = "This is so sad." /datum/tgs_chat_command/despacito/Run() - return "https://www.youtube.com/watch?v=kJQP7kiw5Fk" + return new /datum/tgs_message_content("https://www.youtube.com/watch?v=kJQP7kiw5Fk") /datum/tgs_chat_command/polly name = "polly" @@ -187,7 +173,7 @@ GLOBAL_LIST(round_end_notifiees) else var/json_file = file("data/npc_saves/Poly.json") if(!fexists(json_file)) - return "**BAWWWWWK!** LEAVE THE HEADSET! ***BAWKKKKK!!***" + return new /datum/tgs_message_content("**BAWWWWWK!** LEAVE THE HEADSET! ***BAWKKKKK!!***") var/list/json = json_decode(file2text(json_file)) speech_buffer = json["phrases"] - return "[pick(speech_buffer)]" + return new /datum/tgs_message_content("[pick(speech_buffer)]") diff --git a/code/modules/discord/tgs_commands.dm b/code/modules/discord/tgs_commands.dm new file mode 100644 index 0000000000..49b26eb005 --- /dev/null +++ b/code/modules/discord/tgs_commands.dm @@ -0,0 +1,33 @@ +/datum/tgs_chat_command/tgscheck + name = "check" + help_text = "Gets the playercount, gamemode, and address of the server" + +/datum/tgs_chat_command/tgscheck/Run(datum/tgs_chat_user/sender, params) + var/server = CONFIG_GET(string/server) + return new /datum/tgs_message_content("[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]") + +/datum/tgs_chat_command/gameversion + name = "gameversion" + help_text = "Gets the version details from the show-server-revision verb, basically" + +/datum/tgs_chat_command/gameversion/Run(datum/tgs_chat_user/sender, params) + var/list/msg = list("") + msg += "BYOND Server Version: [world.byond_version].[world.byond_build] (Compiled with: [DM_VERSION].[DM_BUILD])\n" + + if (!GLOB.revdata) + msg += "No revision information found." + else + msg += "Revision [copytext_char(GLOB.revdata.commit, 1, 9)]" + if (GLOB.revdata.date) + msg += " compiled on '[GLOB.revdata.date]'" + + if(GLOB.revdata.originmastercommit) + msg += ", from origin commit: <[CONFIG_GET(string/githuburl)]/commit/[GLOB.revdata.originmastercommit]>" + + if(GLOB.revdata.testmerge.len) + msg += "\n" + for(var/datum/tgs_revision_information/test_merge/PR as anything in GLOB.revdata.testmerge) + msg += "PR #[PR.number] at [copytext_char(PR.head_commit, 1, 9)] [PR.title].\n" + if (PR.url) + msg += "<[PR.url]>\n" + return new /datum/tgs_message_content(msg.Join("")) diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index 0c24dd36fa..516ffa1085 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -932,7 +932,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list( /mob/living/simple_animal/parrot/Polly/say(message, bubble_type,var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) . = ..() if(. && !client && prob(1) && prob(1) && CONFIG_GET(string/chat_squawk_tag)) //Only the one true bird may speak across dimensions. - send2chat("A stray squawk is heard... \"[message]\"", CONFIG_GET(string/chat_squawk_tag)) + send2chat(new /datum/tgs_message_content("A stray squawk is heard... \"[message]\""), CONFIG_GET(string/chat_squawk_tag)) /mob/living/simple_animal/parrot/Polly/BiologicalLife(delta_time, times_fired) if(!(. = ..())) diff --git a/modular_sand/code/modules/tgs/chat_commands.dm b/modular_sand/code/modules/tgs/chat_commands.dm index c87de31a3d..6c8611306f 100644 --- a/modular_sand/code/modules/tgs/chat_commands.dm +++ b/modular_sand/code/modules/tgs/chat_commands.dm @@ -3,7 +3,7 @@ help_text = "Lists everyone currently on the server" /datum/tgs_chat_command/who/Run(datum/tgs_chat_user/sender, params) - return tgswho() + return new /datum/tgs_message_content(tgswho()) /datum/tgs_chat_command/awho name = "awho" @@ -11,7 +11,7 @@ admin_only = TRUE /datum/tgs_chat_command/awho/Run(datum/tgs_chat_user/sender, params) - return tgsadminwho() + return new /datum/tgs_message_content(tgsadminwho()) /datum/tgs_chat_command/restart name = "restart" @@ -19,7 +19,7 @@ admin_only = TRUE /datum/tgs_chat_command/restart/Run(datum/tgs_chat_user/sender) - . = "Restarting." + . = new /datum/tgs_message_content("Restarting.") to_chat(world, span_boldwarning("Server restart - Initialized by [sender.friendly_name] on Discord.")) send2adminchat("Server", "[sender.friendly_name] forced a restart.") addtimer(CALLBACK(src, world.TgsEndProcess()), 1 SECONDS) diff --git a/tgstation.dme b/tgstation.dme index d7f975eb98..2dbc56f9c3 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2162,6 +2162,7 @@ #include "code\modules\detectivework\detective_work.dm" #include "code\modules\detectivework\evidence.dm" #include "code\modules\detectivework\scanner.dm" +#include "code\modules\discord\tgs_commands.dm" #include "code\modules\economy\_economy.dm" #include "code\modules\economy\account.dm" #include "code\modules\economy\paystand.dm" From 42d03cb5aeca3dca14937be0beace0ef31a72c1a Mon Sep 17 00:00:00 2001 From: SandPoot Date: Tue, 10 Oct 2023 18:41:41 -0300 Subject: [PATCH 6/6] in all seriousness, this can all be kept --- code/_globalvars/regexes.dm | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/code/_globalvars/regexes.dm b/code/_globalvars/regexes.dm index bd252b68ce..934296fe1b 100644 --- a/code/_globalvars/regexes.dm +++ b/code/_globalvars/regexes.dm @@ -5,3 +5,19 @@ GLOBAL_DATUM_INIT(is_website, /regex, regex("http|www.|\[a-z0-9_-]+.(com|org|net GLOBAL_DATUM_INIT(is_email, /regex, regex("\[a-z0-9_-]+@\[a-z0-9_-]+.\[a-z0-9_-]+", "i")) GLOBAL_DATUM_INIT(is_alphanumeric, /regex, regex("\[a-z0-9]+", "i")) GLOBAL_DATUM_INIT(is_punctuation, /regex, regex("\[.!?]+", "i")) +GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$")) +GLOBAL_DATUM_INIT(is_alpha_color, /regex, regex("^#\[0-9a-fA-F]{8}$")) + +//finds text strings recognized as links on discord. Mainly used to stop embedding. +GLOBAL_DATUM_INIT(has_discord_embeddable_links, /regex, regex("(https?://\[^\\s|<\]{2,})")) + +//All < and > characters +GLOBAL_DATUM_INIT(angular_brackets, /regex, regex(@"[<>]", "g")) + +//All characters between < a > inclusive of the bracket +GLOBAL_DATUM_INIT(html_tags, /regex, regex(@"<.*?>", "g")) + +//All characters forbidden by filenames: ", \, \n, \t, /, ?, %, *, :, |, <, >, .. +GLOBAL_DATUM_INIT(filename_forbidden_chars, /regex, regex(@{""|[\\\n\t/?%*:|<>]|\.\."}, "g")) +GLOBAL_PROTECT(filename_forbidden_chars) +// had to use the OR operator for quotes instead of putting them in the character class because it breaks the syntax highlighting otherwise.