diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index c3a1f6054ed..f6a109df72b 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -541,6 +541,10 @@ /datum/config_entry/string/chat_new_game_notifications default = null +/// validate ownership of admin flags for chat commands +/datum/config_entry/flag/secure_chat_commands + default = FALSE + /datum/config_entry/flag/debug_admin_hrefs /datum/config_entry/number/mc_tick_rate/base_mc_tick_rate diff --git a/code/controllers/subsystem/discord.dm b/code/controllers/subsystem/discord.dm index 56345846dde..a711b0eabed 100644 --- a/code/controllers/subsystem/discord.dm +++ b/code/controllers/subsystem/discord.dm @@ -100,7 +100,7 @@ SUBSYSTEM_DEF(discord) * * lookup_ckey A string representing the ckey to search on */ /datum/controller/subsystem/discord/proc/lookup_id(lookup_ckey) - var/datum/discord_link_record/link = find_discord_link_by_ckey(lookup_ckey) + var/datum/discord_link_record/link = find_discord_link_by_ckey(lookup_ckey, only_valid = TRUE) if(link) return link.discord_id @@ -113,7 +113,7 @@ SUBSYSTEM_DEF(discord) * * lookup_id The discord id as a string */ /datum/controller/subsystem/discord/proc/lookup_ckey(lookup_id) - var/datum/discord_link_record/link = find_discord_link_by_discord_id(lookup_id) + var/datum/discord_link_record/link = find_discord_link_by_discord_id(lookup_id, only_valid = TRUE) if(link) return link.ckey @@ -219,12 +219,15 @@ SUBSYSTEM_DEF(discord) * * Returns a [/datum/discord_link_record] */ -/datum/controller/subsystem/discord/proc/find_discord_link_by_ckey(ckey, timebound = FALSE) +/datum/controller/subsystem/discord/proc/find_discord_link_by_ckey(ckey, timebound = FALSE, only_valid = FALSE) var/timeboundsql = "" if(timebound) timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR" + var/validsql = "" + if(only_valid) + validsql = "AND valid = 1" - var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE ckey = :ckey [timeboundsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1" + var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE ckey = :ckey [timeboundsql] [validsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1" var/datum/db_query/query_get_discord_link_record = SSdbcore.NewQuery( query, list("ckey" = ckey) @@ -254,12 +257,15 @@ SUBSYSTEM_DEF(discord) * * Returns a [/datum/discord_link_record] */ -/datum/controller/subsystem/discord/proc/find_discord_link_by_discord_id(discord_id, timebound = FALSE) +/datum/controller/subsystem/discord/proc/find_discord_link_by_discord_id(discord_id, timebound = FALSE, only_valid = FALSE) var/timeboundsql = "" if(timebound) timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR" + var/validsql = "" + if(only_valid) + validsql = "AND valid = 1" - var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE discord_id = :discord_id [timeboundsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1" + var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE discord_id = :discord_id [timeboundsql] [validsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1" var/datum/db_query/query_get_discord_link_record = SSdbcore.NewQuery( query, list("discord_id" = discord_id) @@ -274,3 +280,22 @@ SUBSYSTEM_DEF(discord) //Make sure we clean up the query qdel(query_get_discord_link_record) + + +/** + * Extract a discord id from a mention string + * + * This will regex out the mention <@num> block to extract the discord id + * + * Arguments: + * * discord_id The users discord mention string (string) + * + * Returns a text string with the discord id or null + */ +/datum/controller/subsystem/discord/proc/get_discord_id_from_mention(mention) + var/static/regex/discord_mention_extraction_regex = regex(@"<@([0-9]+)>") + discord_mention_extraction_regex.Find(mention) + if (length(discord_mention_extraction_regex.group) == 1) + return discord_mention_extraction_regex.group[1] + return null + diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm index ee59609c9a0..128e59d815e 100644 --- a/code/datums/world_topic.dm +++ b/code/datums/world_topic.dm @@ -176,12 +176,10 @@ require_comms_key = TRUE /datum/world_topic/namecheck/Run(list/input) - //Oh this is a hack, someone refactor the functionality out of the chat command PLS - var/datum/tgs_chat_command/namecheck/NC = new - var/datum/tgs_chat_user/user = new - user.friendly_name = input["sender"] - user.mention = user.friendly_name - return NC.Run(user, input["namecheck"]) + log_admin("world/Topic Name Check: [input["sender"]] on [input["namecheck"]]") + message_admins("Name checking [input["namecheck"]] from [input["sender"]] (World topic)") + + return keywords_lookup(input["namecheck"], 1) /datum/world_topic/adminwho keyword = "adminwho" diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm index 064892c90d3..67e2fe3e457 100644 --- a/code/modules/admin/chat_commands.dm +++ b/code/modules/admin/chat_commands.dm @@ -1,57 +1,62 @@ -#define TGS_STATUS_THROTTLE 5 - -/datum/tgs_chat_command/tgsstatus - name = "status" - help_text = "Gets the admincount, playercount, gamemode, and true game mode of the server" +/// Reload admins tgs chat command. Intentionally not validated. +/datum/tgs_chat_command/reload_admins + name = "reload_admins" + help_text = "Forces the server to reload admins." admin_only = TRUE -/datum/tgs_chat_command/tgsstatus/Run(datum/tgs_chat_user/sender, params) - var/list/adm = get_admin_counts() - 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)]). Round has [SSticker.HasRoundStarted() ? "" : "not "]started." - return status +/datum/tgs_chat_command/reload_admins/Run(datum/tgs_chat_user/sender, params) + ReloadAsync() + log_admin("[sender.friendly_name] reloaded admins via chat command.") + message_admins("[sender.friendly_name] reloaded admins via chat command.") + return "Admins reloaded." -/datum/tgs_chat_command/tgscheck - name = "check" - help_text = "Gets the playercount, gamemode, and address of the server" +/datum/tgs_chat_command/reload_admins/proc/ReloadAsync() + set waitfor = FALSE + load_admins() -/datum/tgs_chat_command/tgscheck/Run(datum/tgs_chat_user/sender, params) - 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" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]" +/// subtype tgs chat command with validated admin ranks. Only supports discord. +/datum/tgs_chat_command/validated + var/required_rights = 0 //! validate discord userid is linked to a game admin with these flags. + admin_only = TRUE -/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" +/// called by tgs +/datum/tgs_chat_command/validated/Run(datum/tgs_chat_user/sender, params) + if (!CONFIG_GET(flag/secure_chat_commands) || CONFIG_GET(flag/admin_legacy_system) || !SSdbcore.Connect()) + return Validated_Run(sender, params) - if (!GLOB.revdata) - msg += "No revision information found." + var/discord_id = SSdiscord.get_discord_id_from_mention(sender.mention) || sender.id + if (!discord_id) + return "Error: Unknown error trying to get your discord id." + + var/datum/admins/linked_admin + var/admin_ckey = ckey(SSdiscord.lookup_ckey(discord_id)) + + if (admin_ckey) + linked_admin = GLOB.admin_datums[admin_ckey] || GLOB.deadmins[admin_ckey] 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]>" + return "Error: Could not find a linked ckey for your discord id." - 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 msg.Join("") + if (!linked_admin) + return "Error: Your linked ckey (`[admin_ckey]`) was not found in the admin list. If this is a mistake you can try `reload_admins`" -/datum/tgs_chat_command/ahelp + if (!linked_admin.check_for_rights(required_rights)) + return "Error: Your linked ckey (`[admin_ckey]`) does not have sufficient rights to do that. You require one of the following flags: `[rights2text(required_rights," ")]`" + + return Validated_Run(sender, params) + + +/// Called if the sender passes validation checks or if those checks are disabled. +/datum/tgs_chat_command/validated/proc/Validated_Run(datum/tgs_chat_user/sender, params) + CRASH("[type] has no implementation for Validated_Run()") + +/datum/tgs_chat_command/validated/ahelp name = "ahelp" help_text = " |list>>" admin_only = TRUE + required_rights = R_ADMIN -/datum/tgs_chat_command/ahelp/Run(datum/tgs_chat_user/sender, params) +/datum/tgs_chat_command/validated/ahelp/Validated_Run(datum/tgs_chat_user/sender, params) var/list/all_params = splittext(params, " ") if(all_params.len < 2) return "Insufficient parameters" @@ -66,12 +71,13 @@ return "Ticket #[id] not found!" return TgsPm(target, all_params.Join(" "), sender.friendly_name) -/datum/tgs_chat_command/namecheck +/datum/tgs_chat_command/validated/namecheck name = "namecheck" help_text = "Returns info on the specified target" admin_only = TRUE + required_rights = R_ADMIN -/datum/tgs_chat_command/namecheck/Run(datum/tgs_chat_user/sender, params) +/datum/tgs_chat_command/validated/namecheck/Validated_Run(datum/tgs_chat_user/sender, params) params = trim(params) if(!params) return "Insufficient parameters" @@ -79,34 +85,22 @@ message_admins("Name checking [params] from [sender.friendly_name]") return keywords_lookup(params, 1) -/datum/tgs_chat_command/adminwho +/datum/tgs_chat_command/validated/adminwho name = "adminwho" help_text = "Lists administrators currently on the server" admin_only = TRUE + required_rights = 0 -/datum/tgs_chat_command/adminwho/Run(datum/tgs_chat_user/sender, params) +/datum/tgs_chat_command/validated/adminwho/Validated_Run(datum/tgs_chat_user/sender, params) return tgsadminwho() -GLOBAL_LIST(round_end_notifiees) - -/datum/tgs_chat_command/endnotify - name = "endnotify" - help_text = "Pings the invoker when the round ends" - admin_only = TRUE - -/datum/tgs_chat_command/endnotify/Run(datum/tgs_chat_user/sender, params) - if(!SSticker.IsRoundInProgress() && SSticker.HasRoundStarted()) - return "[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." - -/datum/tgs_chat_command/sdql +/datum/tgs_chat_command/validated/sdql name = "sdql" help_text = "Runs an SDQL query" admin_only = TRUE + required_rights = R_DEBUG -/datum/tgs_chat_command/sdql/Run(datum/tgs_chat_user/sender, params) +/datum/tgs_chat_command/validated/sdql/Validated_Run(datum/tgs_chat_user/sender, params) var/list/results = HandleUserlessSDQL(sender.friendly_name, params) if(!results) return "Query produced no output" @@ -114,16 +108,15 @@ GLOBAL_LIST(round_end_notifiees) var/list/refs = results.len > 3 ? results.Copy(4) : null . = "[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]" -/datum/tgs_chat_command/reload_admins - name = "reload_admins" - help_text = "Forces the server to reload admins." +/datum/tgs_chat_command/validated/tgsstatus + name = "status" + help_text = "Gets the admincount, playercount, gamemode, and true game mode of the server" admin_only = TRUE + required_rights = R_ADMIN -/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." - -/datum/tgs_chat_command/reload_admins/proc/ReloadAsync() - set waitfor = FALSE - load_admins() +/datum/tgs_chat_command/validated/tgsstatus/Validated_Run(datum/tgs_chat_user/sender, params) + var/list/adm = get_admin_counts() + 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)]). Round has [SSticker.HasRoundStarted() ? "" : "not "]started." + return status diff --git a/code/modules/discord/tgs_commands.dm b/code/modules/discord/tgs_commands.dm index 908b2da6964..b1b57a8f430 100644 --- a/code/modules/discord/tgs_commands.dm +++ b/code/modules/discord/tgs_commands.dm @@ -1,3 +1,37 @@ +/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 "[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 msg.Join("") + // Notify /datum/tgs_chat_command/notify name = "notify" diff --git a/config/config.txt b/config/config.txt index 8d4b7d86e68..9ee615a63ed 100644 --- a/config/config.txt +++ b/config/config.txt @@ -501,6 +501,9 @@ MINUTE_CLICK_LIMIT 400 ## Ping users who use the `notify` command when a new game starts. #CHAT_NEW_GAME_NOTIFICATIONS +## Uncomment this to validate admin commands from discord by requiring they come from linked discord accounts and that those discord accounts link to a ckey with the right admin permissions. +# SECURE_CHAT_COMMANDS + ## Allow admin hrefs that don't use the new token system, will eventually be removed #DEBUG_ADMIN_HREFS @@ -559,22 +562,6 @@ DEFAULT_VIEW_SQUARE 15x15 ## of their connection attempt. #ADMIN_2FA_URL https://example.com/id/%ID% -#### DISCORD STUFFS #### -## MAKE SURE ALL SECTIONS OF THIS ARE FILLED OUT BEFORE ENABLING -## Discord IDs can be obtained by following this guide: https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID- - -## Uncomment to enable discord auto-roling when users link their BYOND and Discord accounts -#ENABLE_DISCORD_AUTOROLE - -## Add your discord bot token here. Make sure it has the ability to manage roles -#DISCORD_TOKEN someDiscordToken - -## Add the ID of your guild (server) here -#DISCORD_GUILDID 000000000000000000 - -## Add the ID of the role you want assigning here -#DISCORD_ROLEID 000000000000000000 - ## How long in seconds after which a hard delete is treated as causing lag. This can be a float and supports a precision as low as nanoseconds. #HARD_DELETES_OVERRUN_THRESHOLD 0.5