diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.txt index fcf347d99d3..f55dbd418d3 100644 --- a/SQL/database_changelog.txt +++ b/SQL/database_changelog.txt @@ -1,15 +1,23 @@ Any time you make a change to the schema files, remember to increment the database schema version. Generally increment the minor number, major should be reserved for significant changes to the schema. Both values go up to 255. -The latest database version is 5.1; The query to update the schema revision table is: +The latest database version is 5.2; The query to update the schema revision table is: -INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 1); +INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 2); or -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 1); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 2); In any query remember to add a prefix to the table names if you use one. ---------------------------------------------------- + +Version 5.2, 30 May 2019, by AffectedArc07 +Added a field to the `player` table to track ckey and discord ID relationships + +ALTER TABLE `player` + ADD COLUMN `discord_id` BIGINT NULL DEFAULT NULL AFTER `flags`; +---------------------------------------------------- + Version 5.1, 25 Feb 2018, by MrStonedOne Added four tables to enable storing of stickybans in the database since byond can lose them, and to enable disabling stickybans for a round without depending on a crash free round. Existing stickybans are automagically imported to the tables. diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index 9506e05cac4..44e7639d620 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -320,6 +320,7 @@ CREATE TABLE `player` ( `lastadminrank` varchar(32) NOT NULL DEFAULT 'Player', `accountjoindate` DATE DEFAULT NULL, `flags` smallint(5) unsigned DEFAULT '0' NOT NULL, + `discord_id` BIGINT(20) NULL DEFAULT NULL, PRIMARY KEY (`ckey`), KEY `idx_player_cid_ckey` (`computerid`,`ckey`), KEY `idx_player_ip_ckey` (`ip`,`ckey`) @@ -513,7 +514,6 @@ CREATE TABLE `stickyban_matched_cid` ( PRIMARY KEY (`stickyban`, `matched_cid`) ) ENGINE=InnoDB; - /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql index 5a0a7d481ae..a689ffbb328 100644 --- a/SQL/tgstation_schema_prefixed.sql +++ b/SQL/tgstation_schema_prefixed.sql @@ -320,6 +320,7 @@ CREATE TABLE `SS13_player` ( `lastadminrank` varchar(32) NOT NULL DEFAULT 'Player', `accountjoindate` DATE DEFAULT NULL, `flags` smallint(5) unsigned DEFAULT '0' NOT NULL, + `discord_id` BIGINT(20) NULL DEFAULT NULL, PRIMARY KEY (`ckey`), KEY `idx_player_cid_ckey` (`computerid`,`ckey`), KEY `idx_player_ip_ckey` (`ip`,`ckey`) @@ -513,8 +514,6 @@ CREATE TABLE `SS13_stickyban_matched_cid` ( PRIMARY KEY (`stickyban`, `matched_cid`) ) ENGINE=InnoDB; - - /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index dc045037ada..01bd5c12d0b 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 5 -#define DB_MINOR_VERSION 1 +#define DB_MINOR_VERSION 2 //Timing subsystem @@ -78,6 +78,7 @@ #define INIT_ORDER_SHUTTLE -21 #define INIT_ORDER_MINOR_MAPPING -40 #define INIT_ORDER_PATH -50 +#define INIT_ORDER_DISCORD -60 #define INIT_ORDER_PERSISTENCE -100 // Subsystem fire priority, from lowest to highest priority diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 1959cf35cf6..97e35f6491e 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -164,8 +164,6 @@ to_chat(world, "


The round has ended.") log_game("The round has ended.") - if(LAZYLEN(GLOB.round_end_notifiees)) - send2irc("Notice", "[GLOB.round_end_notifiees.Join(", ")] the round has ended.") for(var/I in round_end_events) var/datum/callback/cb = I diff --git a/code/controllers/subsystem/discord.dm b/code/controllers/subsystem/discord.dm new file mode 100644 index 00000000000..73105a33217 --- /dev/null +++ b/code/controllers/subsystem/discord.dm @@ -0,0 +1,115 @@ +/* +NOTES: +There is a DB table to track ckeys and associated discord IDs. +This system REQUIRES TGS, and will auto-disable if TGS is not present. +The SS uses fire() instead of just pure shutdown, so people can be notified if it comes back after a crash, where the SS wasnt properly shutdown +It only writes to the disk every 5 minutes, and it wont 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 THIS 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 hasnt 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 + + var/list/notify_members = list() // People to save to notify file + var/list/notify_members_cache = list() // Copy of previous list, so the SS doesnt have to fire if no new members have been added + var/list/people_to_notify = list() // People to notify on roundstart + var/list/account_link_cache = list() // List that holds accounts to link, used in conjunction with TGS + var/notify_file = file("data/notify.json") + var/enabled = 0 // Is TGS enabled (If not we wont fire because otherwise this is useless) + +/datum/controller/subsystem/discord/Initialize(start_timeofday) + // Check for if we are using TGS, otherwise return and disabless firing + if(world.TgsAvailable()) + enabled = 1 // Allows other procs to use this (Account linking, etc) + else + can_fire = 0 // We dont want excess firing + return ..() // Cancel + + try + people_to_notify = json_decode(file2text(notify_file)) + catch + pass() // The list can just stay as its defualt (blank). Pass() exists because it needs a catch + var/notifymsg = "" + for(var/id in people_to_notify) + // I would use jointext here, but I dont think you can two-side glue with it, and I would have to strip characters otherwise + notifymsg += "<@[id]> " // 22 charaters per notify, 90 notifies per message, so I am not making a failsafe because 90 people arent going to notify at once + if(notifymsg) + send2chat("[notifymsg]", CONFIG_GET(string/chat_announce_new_game)) // Sends the message to the discord, using same config option as the roundstart notification + fdel(notify_file) // Deletes the file + return ..() + +/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 + +// Returns ID from ckey +/datum/controller/subsystem/discord/proc/lookup_id(lookup_ckey) + var/datum/DBQuery/query_get_discord_id = SSdbcore.NewQuery("SELECT discord_id FROM [format_table_name("player")] WHERE ckey = '[sanitizeSQL(lookup_ckey)]'") + if(!query_get_discord_id.Execute()) + qdel(query_get_discord_id) + return + if(query_get_discord_id.NextRow()) + . = query_get_discord_id.item[1] + qdel(query_get_discord_id) + +// Returns ckey from ID +/datum/controller/subsystem/discord/proc/lookup_ckey(lookup_id) + var/datum/DBQuery/query_get_discord_ckey = SSdbcore.NewQuery("SELECT ckey FROM [format_table_name("player")] WHERE discord_id = '[sanitizeSQL(lookup_id)]'") + if(!query_get_discord_ckey.Execute()) + qdel(query_get_discord_ckey) + return + if(query_get_discord_ckey.NextRow()) + . = query_get_discord_ckey.item[1] + qdel(query_get_discord_ckey) + +// Finalises link +/datum/controller/subsystem/discord/proc/link_account(ckey) + var/datum/DBQuery/link_account = SSdbcore.NewQuery("UPDATE [format_table_name("player")] SET discord_id = '[sanitizeSQL(account_link_cache[ckey])]' WHERE ckey = '[sanitizeSQL(ckey)]'") + link_account.Execute() + qdel(link_account) + account_link_cache -= ckey + +// Unlink account (Admin verb used) +/datum/controller/subsystem/discord/proc/unlink_account(ckey) + var/datum/DBQuery/unlink_account = SSdbcore.NewQuery("UPDATE [format_table_name("player")] SET discord_id = NULL WHERE ckey = '[sanitizeSQL(ckey)]'") + unlink_account.Execute() + qdel(unlink_account) + +// Clean up a discord account mention +/datum/controller/subsystem/discord/proc/id_clean(input) + var/regex/num_only = regex("\[^0-9\]", "g") + return num_only.Replace(input, "") diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index f84d3667b9f..8d0021e9c66 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -72,6 +72,7 @@ GLOBAL_PROTECT(admin_verbs_admin) /client/proc/resetasaycolor, /client/proc/toggleadminhelpsound, /client/proc/respawn_character, + /client/proc/discord_id_manipulation, /datum/admins/proc/open_borgopanel ) GLOBAL_LIST_INIT(admin_verbs_ban, list(/client/proc/unban_panel, /client/proc/ban_panel, /client/proc/stickybanpanel)) diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm index b222d0d8a60..e66459e951a 100644 --- a/code/modules/admin/chat_commands.dm +++ b/code/modules/admin/chat_commands.dm @@ -75,12 +75,12 @@ GLOBAL_LIST(round_end_notifiees) -/datum/tgs_chat_command/notify - name = "notify" +/datum/tgs_chat_command/endnotify + name = "endnotify" help_text = "Pings the invoker when the round ends" admin_only = TRUE -/datum/tgs_chat_command/notify/Run(datum/tgs_chat_user/sender, params) +/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) diff --git a/code/modules/discord/accountlink.dm b/code/modules/discord/accountlink.dm new file mode 100644 index 00000000000..df77cc5e422 --- /dev/null +++ b/code/modules/discord/accountlink.dm @@ -0,0 +1,45 @@ +// Verb to link discord accounts to BYOND accounts +/client/verb/linkdiscord() + set category = "Special Verbs" + set name = "Link Discord Account" + set desc = "Link your discord account to your BYOND account." + + // Safety checks + if(!CONFIG_GET(flag/sql_enabled)) + to_chat(src, "This feature requires the SQL backend to be running.") + return + + if(!SSdiscord) // SS is still starting + to_chat(src, "The server is still starting up. Please wait before attempting to link your account ") + return + + if(!SSdiscord.enabled) + to_chat(src, "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 + var/know_how = alert("Do you know how to get a discord user ID? This ID is NOT your discord username and numbers! (Pressing NO will open a guide)","Question","Yes","No","Cancel Linking") + if(know_how == "No") // Opens discord support on how to collect IDs + src << link("https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID") + if(know_how == "Cancel Linking") + return + var/entered_id = input("Please enter your Discord ID (18-ish digits)", "Enter Discord ID", null, null) as text|null + SSdiscord.account_link_cache[replacetext(lowertext(usr.ckey), " ", "")] = "[entered_id]" // Prepares for TGS-side verification, also fuck spaces + alert(usr, "Account link started. Please ping the bot of the server you\'re currently on, follows by \"verify [usr.ckey]\" in the Discord to successfuly verify your account (Example: @Mr_Terry verify [usr.ckey])") + + else // Account is already linked + var/choice = alert("You already have the Discord Account [stored_id] linked to [usr.ckey]. Would you like to link a different account?","Already Linked","Yes","No") + if(choice == "Yes") + var/know_how = alert("Do you know how to get a discord user ID? This ID is NOT your discord username and numbers! (Pressing NO will open a guide)","Question","Yes","No", "Cancel Linking") + if(know_how == "No") // Opens discord support on how to collect IDs + src << link("https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID") + + if(know_how == "Cancel Linking") + return + + var/entered_id = input("Please enter your Discord ID (18-ish digits)", "Enter Discord ID", null, null) as text|null + SSdiscord.account_link_cache[replacetext(lowertext(usr.ckey), " ", "")] = "[entered_id]" // Prepares for TGS-side verification, also fuck spaces + alert(usr, "Account link started. Please ping the bot of the server you\'re currently on, followed by \"verify [usr.ckey]\" in the Discord to successfuly verify your account (Example: @Mr_Terry verify [usr.ckey])") + // This is so people cant fill the notify list with a fuckload of ckeys + SSdiscord.notify_members -= "[stored_id]" // The list uses strings because BYOND cannot handle a 17 digit integer diff --git a/code/modules/discord/manipulation.dm b/code/modules/discord/manipulation.dm new file mode 100644 index 00000000000..3280674626e --- /dev/null +++ b/code/modules/discord/manipulation.dm @@ -0,0 +1,36 @@ +// Verb to manipulate IDs and ckeys +/client/proc/discord_id_manipulation() + set name = "Discord Manipulation" + set category = "Admin" + + if(!check_rights(R_ADMIN)) + return + + holder.discord_manipulation() + + +/datum/admins/proc/discord_manipulation() + if(!usr.client.holder) + return + + if(!SSdiscord.enabled) + to_chat(usr, "TGS is not enabled") + return + + var/lookup_choice = alert(usr, "Do you wish to lookup account by ID or ckey?", "Lookup Type", "ID", "Ckey", "Cancel") + switch(lookup_choice) + if("ID") + var/lookup_id = input(usr,"Enter Discord ID to lookup ckey") as text + var/returned_ckey = SSdiscord.lookup_id(lookup_id) + if(returned_ckey) + var/unlink_choice = alert(usr, "Discord ID [lookup_id] is linked to Ckey [returned_ckey]. Do you wish to unlink or cancel?", "Account Found", "Unlink", "Cancel") + if(unlink_choice == "Unlink") + SSdiscord.unlink_account(returned_ckey) + else + to_chat(usr, "Discord ID [lookup_id] has no associated ckey") + if("Ckey") + var/lookup_ckey = input(usr,"Enter Ckey to lookup ID") as text + var/returned_id = SSdiscord.lookup_id(lookup_ckey) + if(returned_id) + to_chat(usr, "Ckey [lookup_ckey] is assigned to Discord ID [returned_id]") + to_chat(usr, "Discord mention format: <@[returned_id]>") // < and > print < > in HTML without using them as tags diff --git a/code/modules/discord/tgs_commands.dm b/code/modules/discord/tgs_commands.dm new file mode 100644 index 00000000000..cb2ff65934c --- /dev/null +++ b/code/modules/discord/tgs_commands.dm @@ -0,0 +1,30 @@ +// 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) + for(var/member in SSdiscord.notify_members) // If they are in the list, take them out + if(member == "[sender.mention]") + SSdiscord.notify_members -= "[SSdiscord.id_clean(sender.mention)]" // The list uses strings because BYOND cannot handle a 17 digit integer + return "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 += "[SSdiscord.id_clean(sender.mention)]" // The list uses strings because BYOND cannot handle a 17 digit integer + return "You will now be notified when the server restarts" + +// Verify +/datum/tgs_chat_command/verify + name = "verify" + help_text = "Verifies your discord account and your BYOND account linkage" + +/datum/tgs_chat_command/verify/Run(datum/tgs_chat_user/sender, params) + var/lowerparams = replacetext(lowertext(params), " ", "") // Fuck spaces + if(SSdiscord.account_link_cache[lowerparams]) // First if they are in the list, then if the ckey matches + if(SSdiscord.account_link_cache[lowerparams] == "[SSdiscord.id_clean(sender.mention)]") // If the associated ID is the correct one + SSdiscord.link_account(lowerparams) + return "Successfully linked accounts" + else + return "That ckey is not associated to this discord account. If someone has used your ID, please inform an administrator" + else + return "Account not setup for linkage" diff --git a/code/modules/discord/toggle_notify.dm b/code/modules/discord/toggle_notify.dm new file mode 100644 index 00000000000..5eb1492f757 --- /dev/null +++ b/code/modules/discord/toggle_notify.dm @@ -0,0 +1,34 @@ +// Verb to toggle restart notifications +/client/verb/notify_restart() + set category = "Special Verbs" + 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, "This feature requires the SQL backend to be running.") + return + + if(!SSdiscord) // SS is still starting + to_chat(src, "The server is still starting up. Please wait before attempting to link your account ") + return + + if(!SSdiscord.enabled) + to_chat(src, "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, "This requires you to link your Discord account with the \"Link Discord Account\" verb.") + return + + else // Linked + for(var/member in SSdiscord.notify_members) // If they are in the list, take them out + if(member == "[stored_id]") + SSdiscord.notify_members -= "[stored_id]" // The list uses strings because BYOND cannot handle a 17 digit integer + to_chat(src, "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, "You will now be notified when the server restarts") + SSdiscord.notify_members += "[stored_id]" // The list uses strings because BYOND cannot handle a 17 digit integer diff --git a/config/config.txt b/config/config.txt index 140860924e6..5b985085f9d 100644 --- a/config/config.txt +++ b/config/config.txt @@ -454,7 +454,7 @@ MINUTE_CLICK_LIMIT 400 ## If using TGS4, the string option can be set as a chat channel tag to limit the message to channels of that tag type (case-sensitive) ## i.e. CHAT_ANNOUNCE_NEW_GAME chat_channel_tag -## Send a message with the station name starting a new game +## Send a message with the station name starting a new game. Also required for the notify function #CHAT_ANNOUNCE_NEW_GAME diff --git a/tgstation.dme b/tgstation.dme index 9fdfa1a4c9f..d3b847a59f1 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -230,6 +230,7 @@ #include "code\controllers\subsystem\communications.dm" #include "code\controllers\subsystem\dbcore.dm" #include "code\controllers\subsystem\dcs.dm" +#include "code\controllers\subsystem\discord.dm" #include "code\controllers\subsystem\disease.dm" #include "code\controllers\subsystem\economy.dm" #include "code\controllers\subsystem\events.dm" @@ -1617,6 +1618,10 @@ #include "code\modules\detectivework\evidence.dm" #include "code\modules\detectivework\footprints_and_rag.dm" #include "code\modules\detectivework\scanner.dm" +#include "code\modules\discord\accountlink.dm" +#include "code\modules\discord\manipulation.dm" +#include "code\modules\discord\tgs_commands.dm" +#include "code\modules\discord\toggle_notify.dm" #include "code\modules\economy\_economy.dm" #include "code\modules\economy\account.dm" #include "code\modules\economy\pay_stand.dm"