diff --git a/SQL/database_changelog.md b/SQL/database_changelog.md index 19916b758e327..2b7ccefafbc47 100644 --- a/SQL/database_changelog.md +++ b/SQL/database_changelog.md @@ -3,14 +3,30 @@ Any time you make a change to the schema files, remember to increment the databa The latest database version is 2.0; The query to update the schema revision table is: ```sql -INSERT INTO `schema_revision` (`major`, `minor`) VALUES (2, 3); +INSERT INTO `schema_revision` (`major`, `minor`) VALUES (2, 5); or ``` ```sql -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (2, 3); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (2, 5); ``` In any query remember to add a prefix to the table names if you use one. +---------------------------------------------------- +Version 2.5, 24 February 2025, by TiviPlus +Added discord_links table for discord-ckey verification +```sql +DROP TABLE IF EXISTS `discord_links`; +CREATE TABLE `discord_links` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ckey` VARCHAR(32) NOT NULL, + `discord_id` BIGINT(20) DEFAULT NULL, + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `one_time_token` VARCHAR(100) NOT NULL, + `valid` BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; +``` + ---------------------------------------------------- Version 2.3, 04 February 2025, by TiviPlus Fixed admin rank table flags being capped at 16 in the DB instead of 24 (byond max) diff --git a/SQL/tgmc-schema.sql b/SQL/tgmc-schema.sql index 94c13ebe6f37d..43ecfe1cad8db 100644 --- a/SQL/tgmc-schema.sql +++ b/SQL/tgmc-schema.sql @@ -370,6 +370,20 @@ END $$ DELIMITER ; +-- +-- Table structure for table `discord_links` +-- +DROP TABLE IF EXISTS `discord_links`; +CREATE TABLE `discord_links` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ckey` VARCHAR(32) NOT NULL, + `discord_id` BIGINT(20) DEFAULT NULL, + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `one_time_token` VARCHAR(100) NOT NULL, + `valid` BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + DROP TABLE IF EXISTS `tutorial_completions`; CREATE TABLE `tutorial_completions` ( `id` INT NOT NULL AUTO_INCREMENT, diff --git a/code/__DEFINES/_subsystems.dm b/code/__DEFINES/_subsystems.dm index faebd413bdc79..f021f4c866ab1 100644 --- a/code/__DEFINES/_subsystems.dm +++ b/code/__DEFINES/_subsystems.dm @@ -1,7 +1,7 @@ //Update this whenever the db schema changes //make sure you add an update to the schema_version stable in the db changelog #define DB_MAJOR_VERSION 2 -#define DB_MINOR_VERSION 3 +#define DB_MINOR_VERSION 5 //Timing subsystem //Don't run if there is an identical unique timer active @@ -86,23 +86,24 @@ #define INIT_ORDER_SECURITY_LEVEL 18 #define INIT_ORDER_INSTRUMENTS 17 #define INIT_ORDER_GREYSCALE 16 -#define INIT_ORDER_CODEX 15 -#define INIT_ORDER_EVENTS 14 -#define INIT_ORDER_MONITOR 13 -#define INIT_ORDER_JOBS 12 -#define INIT_ORDER_TICKER 11 -#define INIT_ORDER_MAPPING 10 -#define INIT_ORDER_EARLY_ASSETS 9 -#define INIT_ORDER_SPATIAL_GRID 8 -#define INIT_ORDER_PERSISTENCE 7 //before assets because some assets take data from SSPersistence, such as vendor items -#define INIT_ORDER_TTS 6 -#define INIT_ORDER_ATOMS 5 -#define INIT_ORDER_MODULARMAPPING 4 -#define INIT_ORDER_MACHINES 3 -#define INIT_ORDER_AI_NODES 2 -#define INIT_ORDER_TIMER 1 -#define INIT_ORDER_DEFAULT 0 -#define INIT_ORDER_AIR -1 +#define INIT_ORDER_DISCORD 15 +#define INIT_ORDER_CODEX 14 +#define INIT_ORDER_EVENTS 13 +#define INIT_ORDER_MONITOR 12 +#define INIT_ORDER_JOBS 11 +#define INIT_ORDER_TICKER 10 +#define INIT_ORDER_MAPPING 9 +#define INIT_ORDER_EARLY_ASSETS 8 +#define INIT_ORDER_SPATIAL_GRID 7 +#define INIT_ORDER_PERSISTENCE 6 //before assets because some assets take data from SSPersistence, such as vendor items +#define INIT_ORDER_TTS 5 +#define INIT_ORDER_ATOMS 4 +#define INIT_ORDER_MODULARMAPPING 3 +#define INIT_ORDER_MACHINES 2 +#define INIT_ORDER_AI_NODES 1 +#define INIT_ORDER_TIMER 0 +#define INIT_ORDER_DEFAULT -1 +#define INIT_ORDER_AIR -2 #define INIT_ORDER_ASSETS -4 #define INIT_ORDER_SPAWNING_POOL -5 #define INIT_ORDER_OVERLAY -6 diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index 3bd7d6f62e96e..763418bb5bb27 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -120,3 +120,14 @@ /datum/config_entry/str_list/tts_voice_blacklist /datum/config_entry/flag/give_tutorials_without_db + +/datum/config_entry/str_list/channel_announce_new_game + +/datum/config_entry/str_list/chat_new_game_notifications + +/datum/config_entry/string/discordbotcommandprefix + default = "?" + +/// validate ownership of admin flags for chat commands +/datum/config_entry/flag/secure_chat_commands + default = FALSE diff --git a/code/controllers/subsystem/discord.dm b/code/controllers/subsystem/discord.dm new file mode 100644 index 0000000000000..be9f066cf83c1 --- /dev/null +++ b/code/controllers/subsystem/discord.dm @@ -0,0 +1,303 @@ +/** + * # Discord Subsystem + * + * This subsystem handles some integrations with discord + * + * + * NOTES: + * * There is a DB table to track ckeys and associated discord IDs. (discord_link) + * * This system REQUIRES TGS for notifying users at end of the round + * * The SS uses fire() instead of just pure shutdown, so people can be notified if it comes back after a crash, where the SS wasn't properly shutdown + * * It only writes to the disk every 5 minutes, and it won't write to disk if the file is the same as it was the last time it was written. This is to save on disk writes + * * The system is kept per-server (EG: Terry will not notify people who pressed notify on Sybil), but the accounts are between servers so you dont have to relink on each server. + * + * + * ## HOW NOTIFYING WORKS + * + * ### ROUNDSTART: + * 1) The file is loaded and the discord IDs are extracted + * 2) A ping is sent to the discord with the IDs of people who wished to be notified + * 3) The file is emptied + * + * ### MIDROUND: + * 1) Someone usees the notify verb, it adds their discord ID to the list. + * 2) On fire, it will write that to the disk, as long as conditions above are correct + * + * ### END ROUND: + * 1) The file is force-saved, incase it hasn't fired at end round + * + * This is an absolute clusterfuck, but its my clusterfuck -aa07 + */ +SUBSYSTEM_DEF(discord) + name = "Discord" + wait = 3000 + init_order = INIT_ORDER_DISCORD + + /// People to save to notify file + var/list/notify_members = list() + /// Copy of previous list, so the SS doesnt have to fire if no new members have been added + var/list/notify_members_cache = list() + /// People to notify on roundstart + var/list/people_to_notify = list() + + /// People who have tried to verify this round already + var/list/reverify_cache + + /// Common words list, used to generate one time tokens + var/list/common_words + + /// The file where notification status is saved + var/notify_file = file("data/notify.json") + + /// Is TGS enabled (If not we won't fire because otherwise this is useless) + var/enabled = FALSE + +/datum/controller/subsystem/discord/Initialize() + common_words = world.file2list("strings/1000_most_common.txt") + reverify_cache = list() + // Check for if we are using TGS, otherwise return and disables firing + if(world.TgsAvailable()) + enabled = TRUE // Allows other procs to use this (Account linking, etc) + else + can_fire = FALSE // We dont want excess firing + return SS_INIT_NO_NEED + + try + people_to_notify = json_decode(file2text(notify_file)) + catch + pass() // The list can just stay as its default (blank). Pass() exists because it needs a catch + var/notifymsg = jointext(people_to_notify, ", ") + if(notifymsg) + notifymsg += ", a new round is starting!" + for(var/channel_tag in CONFIG_GET(str_list/chat_new_game_notifications)) + // Sends the message to the discord, using same config option as the roundstart notification + send2chat(new /datum/tgs_message_content(trim(notifymsg)), channel_tag) + fdel(notify_file) // Deletes the file + return SS_INIT_SUCCESS + +/datum/controller/subsystem/discord/fire() + if(!enabled) + return // Dont do shit if its disabled + if(notify_members == notify_members_cache) + return // Dont re-write the file + // If we are all clear + write_notify_file() + +/datum/controller/subsystem/discord/Shutdown() + write_notify_file() // Guaranteed force-write on server close + +/datum/controller/subsystem/discord/proc/write_notify_file() + if(!enabled) // Dont do shit if its disabled + return + fdel(notify_file) // Deletes the file first to make sure it writes properly + WRITE_FILE(notify_file, json_encode(notify_members)) // Writes the file + notify_members_cache = notify_members // Updates the cache list + +/** + * Given a ckey, look up the discord user id attached to the user, if any + * + * This gets the most recent entry from the discord link table that is associated with the given ckey + * + * Arguments: + * * 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, only_valid = TRUE) + if(link) + return link.discord_id + +/** + * Given a discord id as a string, look up the ckey attached to that account, if any + * + * This gets the most recent entry from the discord_link table that is associated with this discord id snowflake + * + * Arguments: + * * 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, only_valid = TRUE) + if(link) + return link.ckey + +/datum/controller/subsystem/discord/proc/get_or_generate_one_time_token_for_ckey(ckey) + // Is there an existing valid one time token + var/datum/discord_link_record/link = find_discord_link_by_ckey(ckey, timebound = TRUE) + if(link) + return link.one_time_token + + // Otherwise we make one + return generate_one_time_token(ckey) + +/** + * Generate a timebound token for discord verification + * + * This uses the common word list to generate a six word random token, this token can then be fed to a discord bot that has access + * to the same database, and it can use it to link a ckey to a discord id, with minimal user effort + * + * It returns the token to the calling proc, after inserting an entry into the discord_link table of the following form + * + * ``` + * (unique_id, ckey, null, the current time, the one time token generated) + * the null value will be filled out with the discord id by the integrated discord bot when a user verifies + * ``` + * + * Notes: + * * The token is guaranteed to unique during its validity period + * * The validity period is currently set at 4 hours + * * a token may not be unique outside its validity window (to reduce conflicts) + * + * Arguments: + * * ckey_for a string representing the ckey this token is for + * + * Returns a string representing the one time token + */ +/datum/controller/subsystem/discord/proc/generate_one_time_token(ckey_for) + + var/not_unique = TRUE + var/one_time_token = "" + // While there's a collision in the token, generate a new one (should rarely happen) + while(not_unique) + //Column is varchar 100, so we trim just in case someone does us the dirty later + one_time_token = trim("[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]", 100) + + not_unique = find_discord_link_by_token(one_time_token, timebound = TRUE) + + // Insert into the table, null in the discord id, id and timestamp and valid fields so the db fills them out where needed + var/datum/db_query/query_insert_link_record = SSdbcore.NewQuery( + "INSERT INTO [format_table_name("discord_links")] (ckey, one_time_token) VALUES(:ckey, :token)", + list("ckey" = ckey_for, "token" = one_time_token) + ) + + if(!query_insert_link_record.Execute()) + qdel(query_insert_link_record) + return "" + + //Cleanup + qdel(query_insert_link_record) + return one_time_token + +/** + * Find discord link entry by the passed in user token + * + * This will look into the discord link table and return the *first* entry that matches the given one time token + * + * Remember, multiple entries can exist, as they are only guaranteed to be unique for their validity period + * + * Arguments: + * * one_time_token the string of words representing the one time token + * * timebound A boolean flag, that specifies if it should only look for entries within the last 4 hours, off by default + * + * Returns a [/datum/discord_link_record] + */ +/datum/controller/subsystem/discord/proc/find_discord_link_by_token(one_time_token, timebound = FALSE) + var/timeboundsql = "" + if(timebound) + timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR" + var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE one_time_token = :one_time_token [timeboundsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1" + var/datum/db_query/query_get_discord_link_record = SSdbcore.NewQuery( + query, + list("one_time_token" = one_time_token) + ) + if(!query_get_discord_link_record.Execute()) + qdel(query_get_discord_link_record) + return + if(query_get_discord_link_record.NextRow()) + var/result = query_get_discord_link_record.item + . = new /datum/discord_link_record(result[2], result[1], result[4], result[3]) + + //Make sure we clean up the query + qdel(query_get_discord_link_record) + +/** + * Find discord link entry by the passed in user ckey + * + * This will look into the discord link table and return the *first* entry that matches the given ckey + * + * Remember, multiple entries can exist + * + * Arguments: + * * ckey the users ckey as a string + * * timebound should we search only in the last 4 hours + * + * Returns a [/datum/discord_link_record] + */ +/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] [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) + ) + if(!query_get_discord_link_record.Execute()) + qdel(query_get_discord_link_record) + return + + if(query_get_discord_link_record.NextRow()) + var/result = query_get_discord_link_record.item + . = new /datum/discord_link_record(result[2], result[1], result[4], result[3]) + + //Make sure we clean up the query + qdel(query_get_discord_link_record) + + +/** + * Find discord link entry by the passed in user ckey + * + * This will look into the discord link table and return the *first* entry that matches the given ckey + * + * Remember, multiple entries can exist + * + * Arguments: + * * discord_id The users discord id (string) + * * timebound should we search only in the last 4 hours + * + * Returns a [/datum/discord_link_record] + */ +/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] [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) + ) + if(!query_get_discord_link_record.Execute()) + qdel(query_get_discord_link_record) + return + + if(query_get_discord_link_record.NextRow()) + var/result = query_get_discord_link_record.item + . = new /datum/discord_link_record(result[2], result[1], result[4], result[3]) + + //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/modules/admin/verbs/chat_commands.dm b/code/modules/admin/verbs/chat_commands.dm index 2427130955a8a..c5d27d471b4d2 100644 --- a/code/modules/admin/verbs/chat_commands.dm +++ b/code/modules/admin/verbs/chat_commands.dm @@ -1,20 +1,3 @@ -#define TGS_STATUS_THROTTLE 7 - -/datum/tgs_chat_command/tgscheck - name = "check" - help_text = "Gets the playercount, gamemode, and address of the server" - var/last_tgs_check = 0 - - -/datum/tgs_chat_command/tgscheck/Run(datum/tgs_chat_user/sender, params) - var/rtod = REALTIMEOFDAY - if(rtod - last_tgs_check < TGS_STATUS_THROTTLE) - return - last_tgs_check = rtod - var/server = CONFIG_GET(string/public_address) || CONFIG_GET(string/server) - return "Round ID: [GLOB.round_id] | Round Time: [gameTimestamp("hh:mm")] | Players: [length(GLOB.clients)] | Ground Map: [length(SSmapping.configs) ? SSmapping.configs[GROUND_MAP].map_name : "Loading..."] | Ship Map: [length(SSmapping.configs) ? SSmapping.configs[SHIP_MAP].map_name : "Loading..."] | Mode: [GLOB.master_mode] | Round Status: [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] | Link: [server ? server : ""]" - - /datum/tgs_chat_command/ahelp name = "ahelp" help_text = " |list>>" @@ -63,20 +46,19 @@ return tgsadminwho() -/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" + return new /datum/tgs_message_content("Query produced no output") var/list/text_res = results.Copy(1, 3) - var/list/refs = results[4] - var/list/names = results[5] - . = "[text_res.Join("\n")][length(refs) ? "\nRefs: [refs.Join(" ")]" : ""][length(names) ? "\nText: [replacetext(names.Join(" "), "
", "")]" : ""]" + var/list/refs = results.len > 3 ? results.Copy(4) : null + return new /datum/tgs_message_content("[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]") /datum/tgs_chat_command/reload_admins @@ -96,31 +78,39 @@ set waitfor = FALSE load_admins() -/datum/tgs_chat_command/lagcheck - name = "lagcheck" - help_text = "Checks current time dilation on the server" - var/last_tgs_check = 0 - - -/datum/tgs_chat_command/lagcheck/Run(datum/tgs_chat_user/sender, params) - var/rtod = REALTIMEOFDAY - if(rtod - last_tgs_check < TGS_STATUS_THROTTLE) - return - last_tgs_check = rtod - return "Time Dilation: [round(SStime_track.time_dilation_current,1)]% AVG:([round(SStime_track.time_dilation_avg_fast,1)]%, [round(SStime_track.time_dilation_avg,1)]%, [round(SStime_track.time_dilation_avg_slow,1)]%)" - -/datum/tgs_chat_command/seasonals - name = "seasonals" - help_text = "Checks current seasonals active in the round." - -/datum/tgs_chat_command/seasonals/Run(datum/tgs_chat_user/sender, params) - var/list/messages = list() - if(!length(SSpersistence.season_progress)) - return new /datum/tgs_message_content("No seasonals found") - for(var/season_entry in SSpersistence.season_progress) - var/season_name = jointext(splittext("[season_entry]", "_"), " ") - var/season_name_first_letter = uppertext(copytext(season_name, 1, 2)) - var/season_name_remainder = copytext(season_name, 2, length(season_name) + 1) - season_name = season_name_first_letter + season_name_remainder - messages += "[season_name]: [SSpersistence.season_progress[season_entry][CURRENT_SEASON_NAME]]" - return new /datum/tgs_message_content(messages.Join("\n")) +/// subtype tgs chat command with validated admin ranks. Only supports discord. +/datum/tgs_chat_command/validated + ignore_type = /datum/tgs_chat_command/validated + admin_only = TRUE + var/required_rights = 0 //! validate discord userid is linked to a game admin with these flags. + +/// 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) + + var/discord_id = SSdiscord.get_discord_id_from_mention(sender.mention) || sender.id + if (!discord_id) + return new /datum/tgs_message_content("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 + return new /datum/tgs_message_content("Error: Could not find a linked ckey for your discord id.") + + if (!linked_admin) + return new /datum/tgs_message_content("Error: Your linked ckey (`[admin_ckey]`) was not found in the admin list. If this is a mistake you can try `reload_admins`") + + if (!linked_admin.check_for_rights(required_rights)) + return new /datum/tgs_message_content("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) + RETURN_TYPE(/datum/tgs_message_content) + CRASH("[type] has no implementation for Validated_Run()") diff --git a/code/modules/discord/accountlink.dm b/code/modules/discord/accountlink.dm new file mode 100644 index 0000000000000..e3227ae90043e --- /dev/null +++ b/code/modules/discord/accountlink.dm @@ -0,0 +1,39 @@ +// IF you have linked your account, this will trigger a verify of the user +/client/verb/verify_in_discord() + set category = "OOC" + set name = "Verify Discord Account" + set desc = "Verify your discord account with your BYOND account" + + // Safety checks + if(!CONFIG_GET(flag/sql_enabled)) + to_chat(src, span_warning("This feature requires the SQL backend to be running.")) + return + + // Why this would ever be unset, who knows + var/prefix = CONFIG_GET(string/discordbotcommandprefix) + if(!prefix) + to_chat(src, span_warning("This feature is disabled.")) + return + + if(!SSdiscord || !SSdiscord.reverify_cache) + to_chat(src, span_warning("Wait for the Discord subsystem to finish initialising")) + return + var/message = "" + // Simple sanity check to prevent a user doing this too often + var/cached_one_time_token = SSdiscord.reverify_cache[usr.ckey] + if(cached_one_time_token && cached_one_time_token != "") + message = "You already generated your one time token, it is [cached_one_time_token]. If you need a new one, you will have to wait until the round ends, or switch to another server; try verifying yourself on Discord by copying this command: [prefix]verify [cached_one_time_token] and pasting it into the verification channel." + + + else + // Will generate one if an expired one doesn't exist already, otherwise will grab existing token + var/one_time_token = SSdiscord.get_or_generate_one_time_token_for_ckey(ckey) + SSdiscord.reverify_cache[usr.ckey] = one_time_token + message = "Your one time token is: [one_time_token]. Assuming you have the required living minutes in game, you can now verify yourself on Discord by using the command: [prefix]verify [one_time_token]" + + //Now give them a browse window so they can't miss whatever we told them + var/datum/browser/window = new/datum/browser(usr, "discordverification", "Discord Verification") + window.set_content("
[message]
") + window.open() + + diff --git a/code/modules/discord/discord_embed.dm b/code/modules/discord/discord_embed.dm new file mode 100644 index 0000000000000..cae7d9ade6ad2 --- /dev/null +++ b/code/modules/discord/discord_embed.dm @@ -0,0 +1,80 @@ +/// Documentation for the embed object and all of its variables can be found at +/// https://discord.com/developers/docs/resources/channel#embed-object +/// It is recommended to read the documentation on the discord website, as the information below could become outdated in the future. +/datum/discord_embed + /// Title of the embed + var/title + /// The description + var/description + /// The URL that the title + var/url + /// The colour that appears on the top of the embed. This is an integer and is the color code of the embed. + var/color + /// The footer that appears on the embed + var/footer + /// String representing a link to an image + var/image + /// String representing a link to the thumbnail image + var/thumbnail + /// String representing a link to the video + var/video + /// String representing the name of the provider + var/provider + /// String representing the link of the provider + var/provider_url + /// Name of the author of the embed + var/author + /// A key-value string list of fields that should be displayed + var/list/fields + /// Any content that should appear above the embed + var/content + +/datum/discord_embed/proc/convert_to_list() + if(color && !isnum(color)) + CRASH("Color on [type] is not a number! Expected a number, got [color] instead.") + var/list/data_to_list = list() + if(title) + data_to_list["title"] = title + if(description) + var/new_desc = replacetext(replacetext(description, "\proper", ""), "\improper", "") + new_desc = GLOB.has_discord_embeddable_links.Replace(replacetext(new_desc, "`", ""), " ```$1``` ") + data_to_list["description"] = new_desc + if(url) + data_to_list["url"] = url + if(color) + data_to_list["color"] = color + if(footer) + data_to_list["footer"] = list( + "text" = footer, + ) + if(image) + data_to_list["image"] = list( + "url" = image, + ) + if(thumbnail) + data_to_list["thumbnail"] = list( + "url" = thumbnail, + ) + if(video) + data_to_list["video"] = list( + "url" = video, + ) + if(provider) + data_to_list["provider"] = list( + "name" = provider, + "url" = provider_url, + ) + if(author) + data_to_list["author"] = list( + "author" = author, + ) + if(fields) + data_to_list["fields"] = list() + for(var/data as anything in fields) + if(!fields[data]) + continue + data_to_list["fields"] += list(list( + "name" = data, + "value" = GLOB.has_discord_embeddable_links.Replace(replacetext(fields[data], "`", ""), " ```$1``` "), + )) + return data_to_list diff --git a/code/modules/discord/discord_link_record.dm b/code/modules/discord/discord_link_record.dm new file mode 100644 index 0000000000000..23aff5dac3825 --- /dev/null +++ b/code/modules/discord/discord_link_record.dm @@ -0,0 +1,24 @@ +/// Represents a record from the discord link table in a nicer format +/datum/discord_link_record + var/ckey + var/discord_id + var/one_time_token + var/timestamp + +/** + * Generate a discord link datum from the values + * + * This is only used by SSdiscord wrapper functions for now, so you can reference the fields + * slightly easier + * + * Arguments: + * * ckey Ckey as a string + * * discord_id Discord id as a string + * * one_time_token as a string + * * timestamp as a string + */ +/datum/discord_link_record/New(ckey, discord_id, one_time_token, timestamp) + src.ckey = ckey + src.discord_id = discord_id + src.one_time_token = one_time_token + src.timestamp = timestamp diff --git a/code/modules/discord/tgs_commands.dm b/code/modules/discord/tgs_commands.dm new file mode 100644 index 0000000000000..e999caa52c574 --- /dev/null +++ b/code/modules/discord/tgs_commands.dm @@ -0,0 +1,75 @@ + +/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/public_address) || CONFIG_GET(string/server) + return "Round ID: [GLOB.round_id] | Round Time: [gameTimestamp("hh:mm")] | Players: [length(GLOB.clients)] | Ground Map: [length(SSmapping.configs) ? SSmapping.configs[GROUND_MAP].map_name : "Loading..."] | Ship Map: [length(SSmapping.configs) ? SSmapping.configs[SHIP_MAP].map_name : "Loading..."] | Mode: [GLOB.master_mode] | Round Status: [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] | Link: [server ? server : ""]" + +/datum/tgs_chat_command/lagcheck + name = "lagcheck" + help_text = "Checks current time dilation on the server" + +/datum/tgs_chat_command/lagcheck/Run(datum/tgs_chat_user/sender, params) + return "Time Dilation: [round(SStime_track.time_dilation_current,1)]% AVG:([round(SStime_track.time_dilation_avg_fast,1)]%, [round(SStime_track.time_dilation_avg,1)]%, [round(SStime_track.time_dilation_avg_slow,1)]%)" + +/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("")) + +// Notify +/datum/tgs_chat_command/notify + name = "notify" + help_text = "Pings the invoker when the round ends" + +/datum/tgs_chat_command/notify/Run(datum/tgs_chat_user/sender, params) + if(!CONFIG_GET(str_list/channel_announce_new_game)) + return new /datum/tgs_message_content("Notifcations are currently disabled") + + for(var/member in SSdiscord.notify_members) // If they are in the list, take them out + if(member == sender.mention) + SSdiscord.notify_members -= sender.mention + return new /datum/tgs_message_content("You will no longer be notified when the server restarts") + + // If we got here, they arent in the list. Chuck 'em in! + SSdiscord.notify_members += sender.mention + return new /datum/tgs_message_content("You will now be notified when the server restarts") + +/datum/tgs_chat_command/seasonals + name = "seasonals" + help_text = "Checks current seasonals active in the round." + +/datum/tgs_chat_command/seasonals/Run(datum/tgs_chat_user/sender, params) + var/list/messages = list() + if(!length(SSpersistence.season_progress)) + return new /datum/tgs_message_content("No seasonals found") + for(var/season_entry in SSpersistence.season_progress) + var/season_name = jointext(splittext("[season_entry]", "_"), " ") + var/season_name_first_letter = uppertext(copytext(season_name, 1, 2)) + var/season_name_remainder = copytext(season_name, 2, length(season_name) + 1) + season_name = season_name_first_letter + season_name_remainder + messages += "[season_name]: [SSpersistence.season_progress[season_entry][CURRENT_SEASON_NAME]]" + return new /datum/tgs_message_content(messages.Join("\n")) diff --git a/code/modules/discord/toggle_notify.dm b/code/modules/discord/toggle_notify.dm new file mode 100644 index 0000000000000..b33c18d950c90 --- /dev/null +++ b/code/modules/discord/toggle_notify.dm @@ -0,0 +1,34 @@ +// Verb to toggle restart notifications +/client/verb/notify_restart() + set category = "OOC" + set name = "Notify Restart" + set desc = "Notifies you on Discord when the server restarts." + + // Safety checks + if(!CONFIG_GET(flag/sql_enabled)) + to_chat(src, span_warning("This feature requires the SQL backend to be running.")) + return + + if(!SSdiscord) // SS is still starting + to_chat(src, span_notice("The server is still starting up. Please wait before attempting to link your account ")) + return + + if(!SSdiscord.enabled) + to_chat(src, span_warning("This feature requires the server is running on the TGS toolkit")) + return + + var/stored_id = SSdiscord.lookup_id(usr.ckey) + if(!stored_id) // Account is not linked + to_chat(src, span_warning("This requires you to link your Discord account with the \"Link Discord Account\" verb.")) + return + + var/stored_mention = "<@[stored_id]>" + for(var/member in SSdiscord.notify_members) // If they are in the list, take them out + if(member == stored_mention) + SSdiscord.notify_members -= stored_mention + to_chat(src, span_notice("You will no longer be notified when the server restarts")) + return // This is necassary so it doesnt get added again, as it relies on the for loop being unsuccessful to tell us if they are in the list or not + + // If we got here, they arent in the list. Chuck 'em in! + to_chat(src, span_notice("You will now be notified when the server restarts")) + SSdiscord.notify_members += "[stored_mention]" diff --git a/config/config.txt b/config/config.txt index 029da3e9a01ef..f03b80fd08916 100644 --- a/config/config.txt +++ b/config/config.txt @@ -245,3 +245,15 @@ CACHE_ASSETS 0 ## This is primarily useful for developing tutorials. If you have a proper DB setup, you ## don't need (or want) this. #GIVE_TUTORIALS_WITHOUT_DB + +## Discord bot command prefix, if the discord bot is used +# DISCORDBOTCOMMANDPREFIX ? + +## Which channels will have a message about a new game starting and ping people +#CHANNEL_ANNOUNCE_NEW_GAME + +## Ping users who use the `notify` command when a new game starts. +#CHAT_NEW_GAME_NOTIFICATIONS + +## Uncomment this flag 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 diff --git a/strings/1000_most_common.txt b/strings/1000_most_common.txt new file mode 100644 index 0000000000000..adbe0f6fcf091 --- /dev/null +++ b/strings/1000_most_common.txt @@ -0,0 +1,1035 @@ +a +able +about +above +accept +across +act +actually +add +admit +afraid +after +afternoon +again +against +age +ago +agree +ah +ahead +air +all +allow +almost +alone +along +already +alright +also +although +always +am +amaze +an +and +anger +angry +animal +annoy +another +answer +any +anymore +anyone +anything +anyway +apartment +apparently +appear +approach +are +area +aren't +arent +arm +around +arrive +as +ask +asleep +ass +at +attack +attempt +attention +aunt +avoid +away +baby +back +bad +bag +ball +band +bar +barely +bathroom +be +beat +beautiful +became +because +become +bed +bedroom +been +before +began +begin +behind +believe +bell +beside +besides +best +better +between +big +bit +bite +black +blink +block +blonde +blood +blue +blush +body +book +bore +both +bother +bottle +bottom +box +boy +boyfriend +brain +break +breakfast +breath +breathe +bright +bring +broke +broken +brother +brought +brown +brush +build +burn +burst +bus +business +busy +but +buy +by +call +calm +came +can +can't +cant +car +card +care +carefully +carry +case +cat +catch +caught +cause +cell +chair +chance +change +chase +check +cheek +chest +child +children +chuckle +city +class +clean +clear +climb +close +clothes +coffee +cold +college +color +come +comment +complete +completely +computer +concern +confuse +consider +continue +control +conversation +cool +corner +couch +could +couldn't +couldnt +counter +couple +course +cover +crack +crazy +cross +crowd +cry +cup +cut +cute +dad +damn +dance +dark +date +daughter +day +dead +deal +dear +death +decide +deep +definitely +desk +did +didn't +didnt +die +different +dinner +direction +disappear +do +doctor +does +doesn't +doesnt +dog +don't +done +dont +door +doubt +down +drag +draw +dream +dress +drink +drive +drop +drove +dry +during +each +ear +early +easily +easy +eat +edge +either +else +empty +end +enjoy +enough +enter +entire +escape +especially +even +evening +eventually +ever +every +everyone +everything +exactly +except +excite +exclaim +excuse +expect +explain +expression +eye +eyebrow +face +fact +fall +family +far +fast +father +fault +favorite +fear +feel +feet +fell +felt +few +field +fight +figure +fill +finally +find +fine +finger +finish +fire +first +fit +five +fix +flash +flip +floor +fly +focus +follow +food +foot +for +force +forget +form +forward +found +four +free +friend +from +front +frown +fuck +full +fun +funny +further +game +gasp +gave +gaze +gently +get +giggle +girl +girlfriend +give +given +glad +glance +glare +glass +go +God +gone +gonna +good +got +gotten +grab +great +green +greet +grey +grin +grip +groan +ground +group +grow +guard +guess +gun +guy +had +hadn't +hadnt +hair +half +hall +hallway +hand +handle +hang +happen +happy +hard +has +hate +have +haven't +havent +he +he'd +he's +head +hear +heard +heart +heavy +hed +held +hell +hello +help +her +here +herself +hes +hey +hi +hide +high +him +himself +his +hit +hold +home +hope +horse +hospital +hot +hour +house +how +however +hug +huge +huh +human +hundred +hung +hurry +hurt +I +I'd +I'll +I'm +I've +ice +Id +idea +if +ignore +Ill +Im +imagine +immediately +important +in +inside +instead +interest +interrupt +into +is +isn't +isnt +it +it's +its +its +Ive +jacket +jeans +jerk +job +join +joke +jump +just +keep +kept +key +kick +kid +kill +kind +kiss +kitchen +knee +knew +knock +know +known +lady +land +large +last +late +laugh +lay +lead +lean +learn +least +leave +led +left +leg +less +let +letter +lie +life +lift +light +like +line +lip +listen +little +live +lock +locker +long +look +lose +lost +lot +loud +love +low +lunch +mad +made +make +man +manage +many +mark +marry +match +matter +may +maybe +me +mean +meant +meet +memory +men +mention +met +middle +might +mind +mine +minute +mirror +miss +mom +moment +money +month +mood +more +morning +most +mother +mouth +move +movie +mr +mrs +much +mum +mumble +music +must +mutter +my +myself +name +near +nearly +neck +need +nervous +never +new +next +nice +night +no +nod +noise +none +normal +nose +not +note +nothing +notice +now +number +obviously +of +off +offer +office +often +oh +okay +old +on +once +one +only +onto +open +or +order +other +our +out +outside +over +own +pack +pain +paint +pair +pants +paper +parents +park +part +party +pass +past +pause +pay +people +perfect +perhaps +person +phone +pick +picture +piece +pink +piss +place +plan +play +please +pocket +point +police +pop +position +possible +power +practically +present +press +pretend +pretty +probably +problem +promise +pull +punch +push +put +question +quick +quickly +quiet +quietly +quite +race +rain +raise +ran +rang +rather +reach +read +ready +real +realize +really +reason +recognize +red +relationship +relax +remain +remember +remind +repeat +reply +respond +rest +return +ride +right +ring +road +rock +roll +room +rose +round +rub +run +rush +sad +safe +said +same +sat +save +saw +say +scare +school +scream +search +seat +second +see +seem +seen +self +send +sense +sent +serious +seriously +set +settle +seven +several +shadow +shake +share +she +she'd +she's +shed +shes +shift +shirt +shit +shock +shoe +shook +shop +short +shot +should +shoulder +shouldn't +shouldnt +shout +shove +show +shower +shrug +shut +sick +side +sigh +sight +sign +silence +silent +simply +since +single +sir +sister +sit +situation +six +skin +sky +slam +sleep +slightly +slip +slow +slowly +small +smell +smile +smirk +smoke +snap +so +soft +softly +some +somehow +someone +something +sometimes +somewhere +son +song +soon +sorry +sort +sound +space +speak +spend +spent +spoke +spot +stair +stand +star +stare +start +state +stay +step +stick +still +stomach +stood +stop +store +story +straight +strange +street +strong +struggle +stuck +student +study +stuff +stupid +such +suck +sudden +suddenly +suggest +summer +sun +suppose +sure +surprise +surround +sweet +table +take +taken +talk +tall +teacher +team +tear +teeth +tell +ten +than +thank +that +that's +thats +the +their +them +themselves +then +there +there's +theres +these +they +they'd +they're +theyd +theyre +thick +thing +think +third +this +those +though +thought +three +threw +throat +through +throw +tie +tight +time +tiny +tire +to +today +together +told +tomorrow +tone +tongue +tonight +too +took +top +totally +touch +toward +town +track +trail +train +tree +trip +trouble +TRUE +trust +truth +try +turn +TV +twenty +two +type +uncle +under +understand +until +up +upon +us +use +usual +usually +very +visit +voice +wait +wake +walk +wall +want +warm +warn +was +wasn't +wasnt +watch +water +wave +way +we +we'll +we're +we've +wear +week +weird +well +well +went +were +were +weren't +werent +wet +weve +what +what's +whatever +whats +when +where +whether +which +while +whisper +white +who +whole +why +wide +wife +will +wind +window +wipe +wish +with +within +without +woke +woman +women +won't +wonder +wont +wood +word +wore +work +world +worry +worse +would +wouldn't +wouldnt +wow +wrap +write +wrong +yeah +year +yell +yes +yet +you +you'd +you'll +you're +you've +youd +youll +young +your +youre +yourself +youve diff --git a/tgmc.dme b/tgmc.dme index 286fbe4e2d639..5e39e8012dbe3 100644 --- a/tgmc.dme +++ b/tgmc.dme @@ -313,6 +313,7 @@ F// DM Environment file for baystation12.dme. #include "code\controllers\subsystem\dbcore.dm" #include "code\controllers\subsystem\dcs.dm" #include "code\controllers\subsystem\direction.dm" +#include "code\controllers\subsystem\discord.dm" #include "code\controllers\subsystem\early_asset.dm" #include "code\controllers\subsystem\egrill.dm" #include "code\controllers\subsystem\evacuation.dm" @@ -1604,6 +1605,11 @@ F// DM Environment file for baystation12.dme. #include "code\modules\detectivework\forensics.dm" #include "code\modules\detectivework\scanner.dm" #include "code\modules\detectivework\scanning_console.dm" +#include "code\modules\discord\accountlink.dm" +#include "code\modules\discord\discord_embed.dm" +#include "code\modules\discord\discord_link_record.dm" +#include "code\modules\discord\tgs_commands.dm" +#include "code\modules\discord\toggle_notify.dm" #include "code\modules\economy\Accounts.dm" #include "code\modules\economy\ATM.dm" #include "code\modules\economy\cash.dm"