diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b4f6e00..3a12438a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin * If there is no section called "Upcoming changes" below this line, please add one with `## Upcoming changes` as the first line, and then a bulleted item directly after with the first change. ## Upcoming Changes +* Overhauled Bans system. Bans are now based on 'identifiers'.(@QuiCM) + * The old Bans table (`Bans`) has been deprecated. New bans will go in `PlayerBans`. Use `/ban convert` to convert to the new system. + * All old ban routes in REST are now redirected. Please use `/v3/bans/*` for REST-based ban management. + * TShock recognizes and acts upon 4 main identifiers: UUID, IP, Player Name, Account name. This can be extended by plugins. + * By default, bans are no longer removed upon expiry or 'deletion'. Instead, they remain in the system. A new ban for an indentifier can be added once an existing ban has expired. +* Server Console now understands Terraria color codes (e.g., `[c/FF00FF:Words]`) and prints the colored text to the console. Note that console colors are limited and thus only approximations. (@QuiCM) +* Fixed a bug in `/sudo` that prevented quoted arguments being forwarded properly. Example: `/sudo /user group "user name" "user group"` should now work correctly. (@QuiCM) +* Shutting down the server should now correctly display the shutdown message to players rather than 'Lost connection'. (@QuiCM) ## TShock 4.4.0 (Pre-release 14) * Terraria v1.4.1.2 (Thanks @Patrikkk and @DeathCradle <3) diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index 3295ac92..e8751dbc 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -1271,15 +1271,15 @@ namespace TShockAPI // ban add [reason] [duration] [flags (default: -a -u -ip)] // Valid flags: -a (ban account name), -u (ban UUID), -n (ban character name), -ip (ban IP address), -e (exact, ban the identifier provided as 'target') // Unless -e is passed to the command, is assumed to be a player or player index. - // ban del - // Target is expected to be an identifier in the format 'identifier_prefix:identifier'. Eg acc:MyAccountName + // ban del + // Target is expected to be a ban Unique ID // ban list [page] // Displays a paginated list of bans - // ban details - // Target is expected to be an identifier in the format 'identifier_prefix:identifier'. Eg acc:MyAccountName - // Output: Banned Identifier - expiration - // Reason: text - // Banned by: name + // ban details + // Target is expected to be a ban Unique ID + // ban convert + // ban-convert-confirm + // Converts all old bans to new ban system. void Help() { @@ -1291,11 +1291,11 @@ namespace TShockAPI args.Player.SendMessage("TShock Ban Help", Color.White); args.Player.SendMessage("Available Ban commands:", Color.White); - args.Player.SendMessage("ban [c/FFAAAA:add] [flags]", Color.White); - args.Player.SendMessage("ban [c/FFAAAA:del] ", Color.White); - args.Player.SendMessage("ban [c/FFAAAA:list]", Color.White); - args.Player.SendMessage("ban [c/FFAAAA:details] ", Color.White); - args.Player.SendMessage("For more info, use [c/AAAAFF:ban help] [c/FFAAAA:command]", Color.White); + args.Player.SendMessage($"ban {"add".Color(Utils.RedHighlight)} [Flags]", Color.White); + args.Player.SendMessage($"ban {"del".Color(Utils.RedHighlight)} ", Color.White); + args.Player.SendMessage($"ban {"list".Color(Utils.RedHighlight)}", Color.White); + args.Player.SendMessage($"ban {"details".Color(Utils.RedHighlight)} ", Color.White); + args.Player.SendMessage($"For more info, use {"ban help".Color(Utils.BoldHighlight)} {"command".Color(Utils.RedHighlight)}", Color.White); } void MoreHelp(string cmd) @@ -1305,49 +1305,76 @@ namespace TShockAPI case "add": args.Player.SendMessage("", Color.White); args.Player.SendMessage("Ban Add Syntax", Color.White); - args.Player.SendMessage("[c/AAAAFF:ban add] [c/FFAAAA:] [[c/AAAAFF:reason]] [[c/FFAAFF:duration]] [[c/AAFFAA:flags]]", Color.White); - args.Player.SendMessage("- [c/FFAAFF:Duration]: uses the format [c/FFAAFF:0d0m0s] to determine the length of the ban. Eg a value of [c/FFAAFF:10d30m0s] would represent 10 days, 30 minutes, 0 seconds.", Color.White); - args.Player.SendMessage("- [c/AAFFAA:flags]: -a (account name), -u (UUID), -n (character name), -ip (IP address), -e (exact, [c/FFAAAA:target] will be treated as identifier)", Color.White); - args.Player.SendMessage(" Unless [c/AAFFAA:-e] is passed to the command, [c/FFAAAA:target] is assumed to be a player or player index", Color.White); - args.Player.SendMessage(" If no [c/AAFFAA:flags] are specified, the command uses [c/AAFFAA:-a -u -ip] by default.", Color.White); - args.Player.SendMessage("Example usage: [c/AAAAFF:ban add] [c/FFAAAA:ExamplePlayer] [c/AAAAFF:\"Cheating\"] 10d30m0s [c/AAFFAA:-a -u -ip]", Color.White); + args.Player.SendMessage($"{"ban add".Color(Utils.BoldHighlight)} <{"Target".Color(Utils.RedHighlight)}> [{"Reason".Color(Utils.BoldHighlight)}] [{"Duration".Color(Utils.PinkHighlight)}] [{"Flags".Color(Utils.GreenHighlight)}]", Color.White); + args.Player.SendMessage($"- {"Duration".Color(Utils.PinkHighlight)}: uses the format {"0d0m0s".Color(Utils.PinkHighlight)} to determine the length of the ban.", Color.White); + args.Player.SendMessage($" Eg a value of {"10d30m0s".Color(Utils.PinkHighlight)} would represent 10 days, 30 minutes, 0 seconds.", Color.White); + args.Player.SendMessage($"- {"Flags".Color(Utils.GreenHighlight)}: -a (account name), -u (UUID), -n (character name), -ip (IP address), -e (exact, {"Target".Color(Utils.RedHighlight)} will be treated as identifier)", Color.White); + args.Player.SendMessage($" Unless {"-e".Color(Utils.GreenHighlight)} is passed to the command, {"Target".Color(Utils.RedHighlight)} is assumed to be a player or player index", Color.White); + args.Player.SendMessage($" If no {"Flags".Color(Utils.GreenHighlight)} are specified, the command uses {"-a -u -ip".Color(Utils.GreenHighlight)} by default.", Color.White); + args.Player.SendMessage($"Example usage: {"ban add".Color(Utils.BoldHighlight)} {args.Player.Name.Color(Utils.RedHighlight)} {"\"Cheating\"".Color(Utils.BoldHighlight)} {"10d30m0s".Color(Utils.PinkHighlight)} {"-a -u -ip".Color(Utils.GreenHighlight)}", Color.White); break; case "del": args.Player.SendMessage("", Color.White); args.Player.SendMessage("Ban Del Syntax", Color.White); - args.Player.SendMessage("[c/AAAAFF:ban del] [c/FFAAAA:target]", Color.White); - args.Player.SendMessage("- [c/FFAAAA:Target] is expected to be an identifier in the format 'identifier_prefix:identifier'. Eg [c/FFAAAA:acc:MyAccountName]", Color.White); - args.Player.SendMessage("Example usage: [c/AAAAFF:ban del] [c/FFAAAA:acc:ExampleAccount]", Color.White); + args.Player.SendMessage($"{"ban del".Color(Utils.BoldHighlight)} <{"Ban ID".Color(Utils.RedHighlight)}>", Color.White); + args.Player.SendMessage($"- {"Ban IDs".Color(Utils.RedHighlight)}s are provided when you add a ban, and can be found with the {"ban list".Color(Utils.BoldHighlight)} command.", Color.White); + args.Player.SendMessage($"Example usage: {"ban del".Color(Utils.BoldHighlight)} {"12345".Color(Utils.RedHighlight)}", Color.White); break; case "list": args.Player.SendMessage("", Color.White); args.Player.SendMessage("Ban List Syntax", Color.White); - args.Player.SendMessage("[c/AAAAFF:ban list] [[c/FFAAFF:page]]", Color.White); + args.Player.SendMessage($"{"ban list".Color(Utils.BoldHighlight)} [{"Page".Color(Utils.PinkHighlight)}]", Color.White); args.Player.SendMessage("- Lists active bans. Color trends towards green as the ban approaches expiration", Color.White); - args.Player.SendMessage("Example usage: [c/AAAAFF:ban list]", Color.White); + args.Player.SendMessage($"Example usage: {"ban list".Color(Utils.BoldHighlight)}", Color.White); break; case "details": args.Player.SendMessage("", Color.White); args.Player.SendMessage("Ban Details Syntax", Color.White); - args.Player.SendMessage("[c/AAAAFF:ban details] [c/FFAAAA:target]", Color.White); - args.Player.SendMessage("- [c/FFAAAA:Target] is expected to be an identifier in the format 'identifier_prefix:identifier'. Eg [c/FFAAAA:acc:MyAccountName]", Color.White); - args.Player.SendMessage("Example usage: [c/AAAAFF:ban details] [c/FFAAAA:acc:ExampleAccount]", Color.White); + args.Player.SendMessage($"{"ban details".Color(Utils.BoldHighlight)} <{"Ban ID".Color(Utils.RedHighlight)}>", Color.White); + args.Player.SendMessage($"- {"Ban IDs".Color(Utils.RedHighlight)}s are provided when you add a ban, and can be found with the {"ban list".Color(Utils.BoldHighlight)} command.", Color.White); + args.Player.SendMessage($"Example usage: {"ban details".Color(Utils.BoldHighlight)} {"12345".Color(Utils.RedHighlight)}", Color.White); break; default: - args.Player.SendMessage("Unknown ban command. Try 'add', 'del', 'list', or 'details'", Color.White); + args.Player.SendMessage($"Unknown ban command. Try {"add".Color(Utils.RedHighlight)}, {"del".Color(Utils.RedHighlight)}, {"list".Color(Utils.RedHighlight)}, or {"details".Color(Utils.RedHighlight)}.", Color.White); break; } } + void DisplayBanDetails(Ban ban) + { + args.Player.SendMessage($"{"Ban Details".Color(Utils.BoldHighlight)} - Unique ID: {ban.UniqueId.Color(Utils.GreenHighlight)}", Color.White); + args.Player.SendMessage($"{"Identifier:".Color(Utils.BoldHighlight)} {ban.Identifier}", Color.White); + args.Player.SendMessage($"{"Reason:".Color(Utils.BoldHighlight)} {ban.Reason}", Color.White); + args.Player.SendMessage($"{"Banned by:".Color(Utils.BoldHighlight)} {ban.BanningUser.Color(Utils.GreenHighlight)} on {ban.BanDateTime.ToString("yyyy/MM/dd").Color(Utils.RedHighlight)} ({ban.GetPrettyTimeSinceBanString().Color(Utils.YellowHighlight)} ago)", Color.White); + if (ban.ExpirationDateTime < DateTime.UtcNow) + { + args.Player.SendMessage($"{"Banned expired:".Color(Utils.BoldHighlight)} {ban.ExpirationDateTime.ToString("yyyy/MM/dd").Color(Utils.RedHighlight)} ({ban.GetPrettyExpirationString().Color(Utils.YellowHighlight)} ago)", Color.White); + } + else + { + string remaining; + if (ban.ExpirationDateTime == DateTime.MaxValue) + { + remaining = "Never".Color(Utils.YellowHighlight); + } + else + { + remaining = $"{ban.GetPrettyExpirationString().Color(Utils.YellowHighlight)} remaining"; + } + + args.Player.SendMessage($"{"Banned expires:".Color(Utils.BoldHighlight)} {ban.ExpirationDateTime.ToString("yyyy/MM/dd").Color(Utils.RedHighlight)} ({remaining})", Color.White); + } + } + void AddBan() { if (!args.Parameters.TryGetValue(1, out string target)) { - args.Player.SendMessage("Invalid Ban Add syntax. Refer to [c/AAAAFF:ban help add] for details on how to use the [c/AAAAFF:ban add] command", Color.White); + args.Player.SendMessage($"Invalid Ban Add syntax. Refer to {"ban help add".Color(Utils.BoldHighlight)} for details on how to use the {"ban add".Color(Utils.BoldHighlight)} command", Color.White); return; } @@ -1357,9 +1384,31 @@ namespace TShockAPI bool banName = args.Parameters.Any(p => p == "-n"); bool banIp = args.Parameters.Any(p => p == "-ip"); - args.Parameters.TryGetValue(2, out string reason); - args.Parameters.TryGetValue(3, out string duration); + string reason = "Banned."; + string duration = null; DateTime expiration = DateTime.MaxValue; + AddBanResult banResult; + + //This is hacky, but because the flag values can go at any parameter, this is forcing the ordering of 'reason' and 'duration' + //while still allowing them to be arbitrarily placed in the parameter list + for (int i = 2; i < args.Parameters.Count; i++) + { + var param = args.Parameters[i]; + if (param != "-e" && param != "-a" && param != "-u" && param != "-n" && param != "-ip") + { + reason = param; + break; + } + } + for (int i = 3; i < args.Parameters.Count; i++) + { + var param = args.Parameters[i]; + if (param != "-e" && param != "-a" && param != "-u" && param != "-n" && param != "-ip") + { + duration = param; + break; + } + } if (TShock.Utils.TryParseTime(duration, out int seconds)) { @@ -1374,13 +1423,15 @@ namespace TShockAPI if (exactTarget) { - if (TShock.Bans.InsertBan(target, reason ?? "Banned", args.Player.Account.Name, DateTime.UtcNow, expiration) != null) + banResult = TShock.Bans.InsertBan(target, reason ?? "Banned", args.Player.Account.Name, DateTime.UtcNow, expiration); + if (banResult.Ban != null) { - args.Player.SendSuccessMessage("Ban added."); + args.Player.SendSuccessMessage($"Ban ID {banResult.Ban.UniqueId} added."); + DisplayBanDetails(banResult.Ban); } else { - args.Player.SendErrorMessage("Failed to insert ban. Ban may already exist, or an error occured."); + args.Player.SendErrorMessage($"Failed to add ban. {banResult.Message}"); } return; } @@ -1400,62 +1451,76 @@ namespace TShockAPI } var player = players[0]; - var identifiers = new List(); - string identifier; + + if (player.HasPermission(Permissions.immunetoban)) + { + args.Player.SendErrorMessage("That player is immune to bans."); + return; + } + + string banReason = null; + void DoBan(string ident) + { + banResult = TShock.Bans.InsertBan(ident, reason, args.Player.Account.Name, DateTime.UtcNow, expiration); + if (banResult.Ban != null) + { + args.Player.SendSuccessMessage($"Ban ID {banResult.Ban.UniqueId} added for identifier {ident}."); + banReason = banResult.Ban.Reason; + } + else + { + args.Player.SendWarningMessage($"Ban skipped for identifier: {ident}"); + args.Player.SendWarningMessage($"Reason: {banResult.Message}"); + } + } if (banAccount) { if (player.Account != null) { - identifier = $"{DB.Ban.Identifiers.Account}{player.Account.Name}"; - if (TShock.Bans.InsertBan(identifier, reason, args.Player.Account.Name, DateTime.UtcNow, expiration) != null) - { - identifiers.Add(identifier); - } + DoBan($"{Identifiers.Account}{player.Account.Name}"); } } if (banUuid) { - identifier = $"{DB.Ban.Identifiers.UUID}{player.UUID}"; - if (TShock.Bans.InsertBan($"{DB.Ban.Identifiers.UUID}{player.UUID}", reason, args.Player.Account.Name, DateTime.UtcNow, expiration) != null) - { - identifiers.Add(identifier); - } + DoBan($"{Identifiers.UUID}{player.UUID}"); } if (banName) { - identifier = $"{DB.Ban.Identifiers.Name}{player.Name}"; - if (TShock.Bans.InsertBan($"{DB.Ban.Identifiers.Name}{player.Name}", reason, args.Player.Account.Name, DateTime.UtcNow, expiration) != null) - { - identifiers.Add(identifier); - } + DoBan($"{Identifiers.Name}{player.Name}"); } if (banIp) { - identifier = $"{DB.Ban.Identifiers.IP}{player.IP}"; - if (TShock.Bans.InsertBan($"{DB.Ban.Identifiers.IP}{player.IP}", reason, args.Player.Account.Name, DateTime.UtcNow, expiration) != null) - { - identifiers.Add(identifier); - } + DoBan($"{Identifiers.IP}{player.IP}"); } - args.Player.SendSuccessMessage("Bans added for identifiers: ", string.Join(", ", identifiers)); + //Using the ban reason to determine if a ban actually happened or not is messy, but it works + if (banReason != null) + { + player.Disconnect($"You have been banned: {banReason}."); + } } void DelBan() { if (!args.Parameters.TryGetValue(1, out string target)) { - args.Player.SendMessage("Invalid Ban Del syntax. Refer to [c/AAAAFF:ban help del] for details on how to use the [c/AAAAFF:ban del] command", Color.White); + args.Player.SendMessage($"Invalid Ban Del syntax. Refer to {"ban help del".Color(Utils.BoldHighlight)} for details on how to use the {"ban del".Color(Utils.BoldHighlight)} command", Color.White); return; } - if (TShock.Bans.RemoveBan(target)) + if (!int.TryParse(target, out int banId)) { - args.Player.SendSuccessMessage("Ban removed."); + args.Player.SendMessage($"Invalid Ban ID. Refer to {"ban help del".Color(Utils.BoldHighlight)} for details on how to use the {"ban del".Color(Utils.BoldHighlight)} command", Color.White); + return; + } + + if (TShock.Bans.RemoveBan(banId)) + { + args.Player.SendSuccessMessage($"Ban {banId} has now been marked as expired."); } else { @@ -1463,26 +1528,37 @@ namespace TShockAPI } } + string PickColorForBan(Ban ban) + { + double hoursRemaining = (ban.ExpirationDateTime - DateTime.UtcNow).TotalHours; + double hoursTotal = (ban.ExpirationDateTime - ban.BanDateTime).TotalHours; + double percentRemaining = TShock.Utils.Clamp(hoursRemaining / hoursTotal, 100, 0); + + int red = TShock.Utils.Clamp((int)(255 * 2.0f * percentRemaining), 255, 0); + int green = TShock.Utils.Clamp((int)(255 * (2.0f * (1 - percentRemaining))), 255, 0); + + return $"{red:X2}{green:X2}{0:X2}"; + } + void ListBans() { - int pageNumber; - if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out pageNumber)) + if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out int pageNumber)) { - args.Player.SendMessage("Invalid Ban List syntax. Refer to [c/AAAAFF:ban help list] for details on how to use the [c/AAAAFF:ban list] command", Color.White); + args.Player.SendMessage($"Invalid Ban List syntax. Refer to {"ban help list".Color(Utils.BoldHighlight)} for details on how to use the {"ban list".Color(Utils.BoldHighlight)} command", Color.White); return; } + + var bans = from ban in TShock.Bans.Bans + where ban.Value.ExpirationDateTime > DateTime.UtcNow + orderby ban.Value.ExpirationDateTime ascending + select $"[{ban.Key.Color(Utils.GreenHighlight)}] {ban.Value.Identifier.Color(PickColorForBan(ban.Value))}"; - List bans = TShock.Bans.GetAllBans(); - - var nameBans = from ban in bans - select ban.Identifier; - - PaginationTools.SendPage(args.Player, pageNumber, PaginationTools.BuildLinesFromTerms(nameBans), + PaginationTools.SendPage(args.Player, pageNumber, bans.ToList(), new PaginationTools.Settings { HeaderFormat = "Bans ({0}/{1}):", FooterFormat = "Type {0}ban list {{0}} for more.".SFormat(Specifier), - NothingToDisplayString = "There are currently no bans." + NothingToDisplayString = "There are currently no active bans." }); } @@ -1490,11 +1566,17 @@ namespace TShockAPI { if (!args.Parameters.TryGetValue(1, out string target)) { - args.Player.SendMessage("Invalid Ban Details syntax. Refer to [c/AAAAFF:ban help details] for details on how to use the [c/AAAAFF:ban details] command", Color.White); + args.Player.SendMessage($"Invalid Ban Details syntax. Refer to {"ban help details".Color(Utils.BoldHighlight)} for details on how to use the {"ban details".Color(Utils.BoldHighlight)} command", Color.White); return; } - Ban ban = TShock.Bans.GetBanByIdentifier(target); + if (!int.TryParse(target, out int banId)) + { + args.Player.SendMessage($"Invalid Ban ID. Refer to {"ban help details".Color(Utils.BoldHighlight)} for details on how to use the {"ban details".Color(Utils.BoldHighlight)} command", Color.White); + return; + } + + Ban ban = TShock.Bans.GetBanById(banId); if (ban == null) { @@ -1502,9 +1584,20 @@ namespace TShockAPI return; } - args.Player.SendMessage($"{ban.Identifier}", Color.White); - args.Player.SendMessage($"Reason: {ban.Reason}", Color.White); - args.Player.SendMessage($"Banned by: [c/AAFFAA:{ban.BanningUser}] at [c/AAAAFF:time]", Color.White); + DisplayBanDetails(ban); + } + + void ConvertBans() + { + args.Player.SendWarningMessage("This will convert all bans from the old ban system to the new identifier-based bans."); + args.Player.SendWarningMessage($"If you are sure you wish to proceed, please execute {"ban-convert-confirm".Color(Utils.WhiteHighlight)} to continue."); + args.Player.AddResponse("ban-convert-confirm", (obj) => + { + TShock.Bans.ConvertBans(); + + var cmdArgs = (CommandArgs)obj; + cmdArgs.Player.SendSuccessMessage("Bans have been converted."); + }); } string subcmd = args.Parameters.Count == 0 ? "help" : args.Parameters[0].ToLower(); @@ -1530,6 +1623,10 @@ namespace TShockAPI BanDetails(); break; + case "convert": + ConvertBans(); + break; + default: break; } @@ -1787,7 +1884,7 @@ namespace TShockAPI return; } - string replacementCommand = String.Join(" ", args.Parameters); + string replacementCommand = String.Join(" ", args.Parameters.Select(p => p.Contains(" ") ? $"\"{p}\"" : p)); args.Player.tempGroup = new SuperAdminGroup(); HandleCommand(args.Player, replacementCommand); args.Player.tempGroup = null; diff --git a/TShockAPI/DB/BanManager.cs b/TShockAPI/DB/BanManager.cs index 06dd8e80..2c8652d9 100644 --- a/TShockAPI/DB/BanManager.cs +++ b/TShockAPI/DB/BanManager.cs @@ -31,14 +31,36 @@ namespace TShockAPI.DB { private IDbConnection database; + private Dictionary _bans; + /// - /// Event invoked when a ban check occurs + /// Dictionary of Bans, keyed on unique ban ID /// - public static event EventHandler OnBanCheck; + public Dictionary Bans + { + get + { + if (_bans == null) + { + _bans = RetrieveAllBans().ToDictionary(b => b.UniqueId); + } + + return _bans; + } + } + /// - /// Event invoked when a ban is added + /// Event invoked when a ban is checked for validity /// - public static event EventHandler OnBanAdded; + public static event EventHandler OnBanValidate; + /// + /// Event invoked before a ban is added + /// + public static event EventHandler OnBanPreAdd; + /// + /// Event invoked after a ban is added + /// + public static event EventHandler OnBanPostAdd; /// /// Initializes a new instance of the class. @@ -49,11 +71,12 @@ namespace TShockAPI.DB database = db; var table = new SqlTable("PlayerBans", - new SqlColumn("Identifier", MySqlDbType.Text) { Primary = true, Unique = true }, + new SqlColumn("Id", MySqlDbType.Int32) { Primary = true, AutoIncrement = true }, + new SqlColumn("Identifier", MySqlDbType.Text), new SqlColumn("Reason", MySqlDbType.Text), new SqlColumn("BanningUser", MySqlDbType.Text), - new SqlColumn("Date", MySqlDbType.Text), - new SqlColumn("Expiration", MySqlDbType.Text) + new SqlColumn("Date", MySqlDbType.Int64), + new SqlColumn("Expiration", MySqlDbType.Int64) ); var creator = new SqlTableCreator(db, db.GetSqlType() == SqlType.Sqlite @@ -69,7 +92,8 @@ namespace TShockAPI.DB throw new Exception("Could not find a database library (probably Sqlite3.dll)"); } - OnBanCheck += CheckBanValid; + OnBanValidate += BanValidateCheck; + OnBanPreAdd += BanAddedCheck; } /// @@ -89,19 +113,29 @@ namespace TShockAPI.DB var date = reader.Get("Date"); var expiration = reader.Get("Expiration"); + if (!DateTime.TryParse(date, out DateTime start)) + { + start = DateTime.UtcNow; + } + + if (!DateTime.TryParse(expiration, out DateTime end)) + { + end = DateTime.MaxValue; + } + if (!string.IsNullOrWhiteSpace(ip)) { - InsertBan($"{Ban.Identifiers.IP}{ip}", reason, banningUser, date, expiration); + InsertBan($"{Identifiers.IP}{ip}", reason, banningUser, start, end); } if (!string.IsNullOrWhiteSpace(account)) { - InsertBan($"{Ban.Identifiers.Account}{account}", reason, banningUser, date, expiration); + InsertBan($"{Identifiers.Account}{account}", reason, banningUser, start, end); } if (!string.IsNullOrWhiteSpace(uuid)) { - InsertBan($"{Ban.Identifiers.UUID}{uuid}", reason, banningUser, date, expiration); + InsertBan($"{Identifiers.UUID}{uuid}", reason, banningUser, start, end); } } } @@ -111,99 +145,145 @@ namespace TShockAPI.DB /// Determines whether or not a ban is valid /// /// + /// /// - public bool IsValidBan(Ban ban) + public bool IsValidBan(Ban ban, TSPlayer player) { - BanEventArgs args = new BanEventArgs { Ban = ban }; - OnBanCheck?.Invoke(this, args); + BanEventArgs args = new BanEventArgs + { + Ban = ban, + Player = player + }; + + OnBanValidate?.Invoke(this, args); return args.Valid; } - internal void CheckBanValid(object sender, BanEventArgs args) + internal void BanValidateCheck(object sender, BanEventArgs args) { - //We consider a ban to be valid if the start time is before now and the end time is after now - args.Valid = args.Ban.BanDateTime < DateTime.UtcNow && args.Ban.ExpirationDateTime > DateTime.UtcNow; + //Only perform validation if the event has not been cancelled before we got here + if (args.Valid) + { + //We consider a ban to be valid if the start time is before now and the end time is after now, and the player is not immune to bans + args.Valid = (DateTime.UtcNow > args.Ban.BanDateTime && DateTime.UtcNow < args.Ban.ExpirationDateTime) && !args.Player.HasPermission(Permissions.immunetoban); + } } - /// - /// Adds a new ban for the given identifier. If the addition succeeds, returns a ban object with the ban details. Else returns null - /// - /// - /// - /// - /// - /// - /// - public Ban InsertBan(string identifier, string reason, string banningUser, DateTime fromDate, DateTime toDate) - => InsertBan(identifier, reason, banningUser, fromDate.ToString("s"), toDate.ToString("s")); - - /// - /// Adds a new ban for the given identifier. If the addition succeeds, returns a ban object with the ban details. Else returns null - /// - /// - /// - /// - /// - /// - /// - public Ban InsertBan(string identifier, string reason, string banningUser, string fromDate, string toDate) + internal void BanAddedCheck(object sender, BanPreAddEventArgs args) { - Ban b = new Ban(identifier, reason, banningUser, fromDate, toDate); + //Only perform validation if the event has not been cancelled before we got here + if (args.Valid) + { + //We consider a ban valid to add if no other *current* bans exist for the identifier provided. + //E.g., if a previous ban has expired, a new ban is valid. + //However, if a previous ban on the provided identifier is still in effect, a new ban is not valid + args.Valid = !Bans.Any(b => b.Value.Identifier == args.Identifier && b.Value.ExpirationDateTime > DateTime.UtcNow); + args.Message = args.Valid ? null : "a current ban for this identifier already exists."; + } + } + + /// + /// Adds a new ban for the given identifier. Returns a Ban object if the ban was added, else null + /// + /// + /// + /// + /// + /// + /// + public AddBanResult InsertBan(string identifier, string reason, string banningUser, DateTime fromDate, DateTime toDate) + { + BanPreAddEventArgs args = new BanPreAddEventArgs + { + Identifier = identifier, + Reason = reason, + BanningUser = banningUser, + BanDateTime = fromDate, + ExpirationDateTime = toDate + }; - BanEventArgs args = new BanEventArgs { Ban = b }; - - OnBanAdded?.Invoke(this, args); + OnBanPreAdd?.Invoke(this, args); if (!args.Valid) { - return null; + string message = $"Ban was invalidated: {(args.Message ?? "no further information provided.")}"; + return new AddBanResult { Message = message }; } - int rowsModified = database.Query("INSERT OR IGNORE INTO PlayerBans (Identifier, Reason, BanningUser, Date, Expiration) VALUES (@0, @1, @2, @3, @4);", identifier, reason, banningUser, fromDate, toDate); - //Return the ban if the query actually inserted the row. If the given identifier already exists, the INSERT is ignored and 0 rows are modified. - return rowsModified != 0 ? b : null; + string query = "INSERT INTO PlayerBans (Identifier, Reason, BanningUser, Date, Expiration) VALUES (@0, @1, @2, @3, @4);"; + + if (database.GetSqlType() == SqlType.Mysql) + { + query += "SELECT CAST(LAST_INSERT_ID() as INT);"; + } + else + { + query += "SELECT CAST(last_insert_rowid() as INT);"; + } + + int uniqueId = database.QueryScalar(query, identifier, reason, banningUser, fromDate.Ticks, toDate.Ticks); + + if (uniqueId == 0) + { + return new AddBanResult { Message = "Database insert failed." }; + } + + Ban b = new Ban(uniqueId, identifier, reason, banningUser, fromDate, toDate); + _bans.Add(uniqueId, b); + + OnBanPostAdd?.Invoke(this, new BanEventArgs { Ban = b }); + + return new AddBanResult { Ban = b }; } /// /// Attempts to remove a ban. Returns true if the ban was removed or expired. False if the ban could not be removed or expired /// - /// + /// The unique ID of the ban to change /// If true, deletes the ban from the database. If false, marks the expiration time as now, rendering the ban expired. Defaults to false /// - public bool RemoveBan(string identifier, bool fullDelete = false) + public bool RemoveBan(int uniqueId, bool fullDelete = false) { int rowsModified; if (fullDelete) { - rowsModified = database.Query("DELETE FROM PlayerBans WHERE Identifier=@0", identifier); + rowsModified = database.Query("DELETE FROM PlayerBans WHERE Id=@0", uniqueId); + _bans.Remove(uniqueId); } else { - rowsModified = database.Query("UPDATE PlayerBans SET Expiration=@0 WHERE Identifier=@1", DateTime.UtcNow.ToString("s"), identifier); + rowsModified = database.Query("UPDATE PlayerBans SET Expiration=@0 WHERE Id=@1", DateTime.UtcNow.Ticks, uniqueId); + _bans[uniqueId].ExpirationDateTime = DateTime.UtcNow; } return rowsModified > 0; } /// - /// Retrieves a ban for a given identifier, or null if no matches are found + /// Retrieves a single ban from a unique ban ID /// - /// + /// /// - public Ban GetBanByIdentifier(string identifier) + public Ban GetBanById(int id) { - using (var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE Identifier=@0", identifier)) + if (Bans.ContainsKey(id)) + { + return Bans[id]; + } + + using (var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE Id=@0", id)) { if (reader.Read()) { - var id = reader.Get("Identifier"); + var uniqueId = reader.Get("Id"); + var identifier = reader.Get("Identifier"); var reason = reader.Get("Reason"); var banningUser = reader.Get("BanningUser"); - var date = reader.Get("Date"); - var expiration = reader.Get("Expiration"); + var date = reader.Get("Date"); + var expiration = reader.Get("Expiration"); - return new Ban(id, reason, banningUser, date, expiration); + return new Ban(uniqueId, identifier, reason, banningUser, date, expiration); } } @@ -211,41 +291,80 @@ namespace TShockAPI.DB } /// - /// Retrieves an enumerable of bans for a given set of identifiers + /// Retrieves an enumerable of all bans for a given identifier /// - /// + /// Identifier to search with + /// Whether or not to exclude expired bans /// - public IEnumerable GetBansByIdentifiers(params string[] identifiers) + public IEnumerable RetrieveBansByIdentifier(string identifier, bool currentOnly = true) { - //Generate a sequence of '@0, @1, @2, ... etc' - var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Count()).Select(p => $"@{p}")); + string query = "SELECT * FROM PlayerBans WHERE Identifier=@0"; + if (currentOnly) + { + query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; + } - using (var reader = database.QueryReader($"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})", identifiers)) + using (var reader = database.QueryReader(query, identifier)) { while (reader.Read()) { + var uniqueId = reader.Get("Id"); + var ident = reader.Get("Identifier"); var id = reader.Get("Identifier"); var reason = reader.Get("Reason"); var banningUser = reader.Get("BanningUser"); - var date = reader.Get("Date"); - var expiration = reader.Get("Expiration"); + var date = reader.Get("Date"); + var expiration = reader.Get("Expiration"); - yield return new Ban(id, reason, banningUser, date, expiration); + yield return new Ban(uniqueId, ident, reason, banningUser, date, expiration); } } } /// - /// Gets a list of bans sorted by their addition date from newest to oldest + /// Retrieves an enumerable of bans for a given set of identifiers /// - public List GetAllBans() => GetAllBansSorted(BanSortMethod.AddedNewestToOldest).ToList(); + /// Whether or not to exclude expired bans + /// + /// + public IEnumerable GetBansByIdentifiers(bool currentOnly = true, params string[] identifiers) + { + //Generate a sequence of '@0, @1, @2, ... etc' + var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Count()).Select(p => $"@{p}")); + + string query = $"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})"; + if (currentOnly) + { + query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; + } + + using (var reader = database.QueryReader(query, identifiers)) + { + while (reader.Read()) + { + var uniqueId = reader.Get("Id"); + var identifier = reader.Get("Identifier"); + var reason = reader.Get("Reason"); + var banningUser = reader.Get("BanningUser"); + var date = reader.Get("Date"); + var expiration = reader.Get("Expiration"); + + yield return new Ban(uniqueId, identifier, reason, banningUser, date, expiration); + } + } + } /// - /// Retrieves an enumerable of objects, sorted using the provided sort method + /// Retrieves a list of bans from the database, sorted by their addition date from newest to oldest + /// + public IEnumerable RetrieveAllBans() => RetrieveAllBansSorted(BanSortMethod.AddedNewestToOldest); + + /// + /// Retrieves an enumerable of s from the database, sorted using the provided sort method /// /// /// - public IEnumerable GetAllBansSorted(BanSortMethod sortMethod) + public IEnumerable RetrieveAllBansSorted(BanSortMethod sortMethod) { List banlist = new List(); try @@ -255,24 +374,25 @@ namespace TShockAPI.DB { while (reader.Read()) { + var uniqueId = reader.Get("Id"); var identifier = reader.Get("Identifier"); var reason = reader.Get("Reason"); var banningUser = reader.Get("BanningUser"); - var date = reader.Get("Date"); - var expiration = reader.Get("Expiration"); + var date = reader.Get("Date"); + var expiration = reader.Get("Expiration"); - var ban = new Ban(identifier, reason, banningUser, date, expiration); + var ban = new Ban(uniqueId, identifier, reason, banningUser, date, expiration); banlist.Add(ban); } } - return banlist; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); Console.WriteLine(ex.StackTrace); } - return null; + + return banlist; } /// @@ -321,51 +441,124 @@ namespace TShockAPI.DB /// /// Bans will be sorted by the date they were added, from oldest to newest /// - AddedOldestToNewest + AddedOldestToNewest, + /// + /// Bans will be sorted by their unique ID + /// + UniqueId } /// - /// Event args used when a ban check is invoked, or a new ban is added + /// Result of an attempt to add a ban + /// + public class AddBanResult + { + /// + /// Message generated from the attempt + /// + public string Message { get; set; } + /// + /// Ban object generated from the attempt, or null if the attempt failed + /// + public Ban Ban { get; set; } + } + + /// + /// Event args used for formalized bans /// public class BanEventArgs : EventArgs { /// - /// The ban being checked or added + /// Complete ban object /// public Ban Ban { get; set; } + /// - /// Whether or not the operation is valid + /// Player ban is being applied to + /// + public TSPlayer Player { get; set; } + + /// + /// Whether or not the operation should be considered to be valid /// public bool Valid { get; set; } = true; } + /// + /// Event args used for ban data prior to a ban being formalized + /// + public class BanPreAddEventArgs : EventArgs + { + /// + /// An identifiable piece of information to ban + /// + public string Identifier { get; set; } + + /// + /// Gets or sets the ban reason. + /// + /// The ban reason. + public string Reason { get; set; } + + /// + /// Gets or sets the name of the user who added this ban entry. + /// + /// The banning user. + public string BanningUser { get; set; } + + /// + /// DateTime from which the ban will take effect + /// + public DateTime BanDateTime { get; set; } + + /// + /// DateTime at which the ban will end + /// + public DateTime ExpirationDateTime { get; set; } + + /// + /// Whether or not the operation should be considered to be valid + /// + public bool Valid { get; set; } = true; + + /// + /// Optional message to explain why the event was invalidated, if it was + /// + public string Message { get; set; } + } + + /// + /// Contains constants for different identifier types known to TShock + /// + public static class Identifiers + { + /// + /// IP identifier prefix constant + /// + public const string IP = "ip:"; + /// + /// UUID identifier prefix constant + /// + public const string UUID = "uuid:"; + /// + /// Player name identifier prefix constant + /// + public const string Name = "name:"; + /// + /// User account identifier prefix constant + /// + public const string Account = "acc:"; + } + /// /// Model class that represents a ban entry in the TShock database. /// public class Ban { /// - /// Contains constants for different identifier types known to TShock + /// A unique ID assigned to this ban /// - public class Identifiers - { - /// - /// IP identifier prefix constant - /// - public const string IP = "ip:"; - /// - /// UUID identifier prefix constant - /// - public const string UUID = "uuid:"; - /// - /// Player name identifier prefix constant - /// - public const string Name = "name:"; - /// - /// User account identifier prefix constant - /// - public const string Account = "acc:"; - } + public int UniqueId { get; set; } /// /// An identifiable piece of information to ban @@ -387,44 +580,71 @@ namespace TShockAPI.DB /// /// DateTime from which the ban will take effect /// - public DateTime BanDateTime { get; } + public DateTime BanDateTime { get; set; } /// /// DateTime at which the ban will end /// - public DateTime ExpirationDateTime { get; } + public DateTime ExpirationDateTime { get; set; } + + /// + /// Returns a string in the format dd:mm:hh:ss indicating the time until the ban expires. + /// If the ban is not set to expire (ExpirationDateTime == DateTime.MaxValue), returns the string 'Never' + /// + /// + public string GetPrettyExpirationString() + { + if (ExpirationDateTime == DateTime.MaxValue) + { + return "Never"; + } + + TimeSpan ts = (ExpirationDateTime - DateTime.UtcNow).Duration(); // Use duration to avoid pesky negatives for expired bans + return $"{ts.Days:00}:{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}"; + } + + /// + /// Returns a string in the format dd:mm:hh:ss indicating the time elapsed since the ban was added. + /// + /// + public string GetPrettyTimeSinceBanString() + { + TimeSpan ts = (DateTime.UtcNow - BanDateTime).Duration(); + return $"{ts.Days:00}:{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}"; + } /// /// Initializes a new instance of the class. /// + /// Unique ID assigned to the ban /// Identifier to apply the ban to /// Reason for the ban /// Account name that executed the ban - /// Ban start datetime - /// Ban end datetime - public Ban(string identifier, string reason, string banningUser, string start, string end) + /// System ticks at which the ban began + /// System ticks at which the ban will end + public Ban(int uniqueId, string identifier, string reason, string banningUser, long start, long end) + : this(uniqueId, identifier, reason, banningUser, new DateTime(start), new DateTime(end)) { + } + + + /// + /// Initializes a new instance of the class. + /// + /// Unique ID assigned to the ban + /// Identifier to apply the ban to + /// Reason for the ban + /// Account name that executed the ban + /// DateTime at which the ban will start + /// DateTime at which the ban will end + public Ban(int uniqueId, string identifier, string reason, string banningUser, DateTime start, DateTime end) + { + UniqueId = uniqueId; Identifier = identifier; Reason = reason; BanningUser = banningUser; - - if (DateTime.TryParse(start, out DateTime d)) - { - BanDateTime = d; - } - else - { - BanDateTime = DateTime.UtcNow; - } - - if (DateTime.TryParse(end, out DateTime e)) - { - ExpirationDateTime = e; - } - else - { - ExpirationDateTime = DateTime.MaxValue; - } + BanDateTime = start; + ExpirationDateTime = end; } } } diff --git a/TShockAPI/Extensions/DbExt.cs b/TShockAPI/Extensions/DbExt.cs index b4e3e0f0..45599136 100644 --- a/TShockAPI/Extensions/DbExt.cs +++ b/TShockAPI/Extensions/DbExt.cs @@ -46,7 +46,6 @@ namespace TShockAPI.DB com.CommandText = query; for (int i = 0; i < args.Length; i++) com.AddParameter("@" + i, args[i]); - return com.ExecuteNonQuery(); } } @@ -81,6 +80,36 @@ namespace TShockAPI.DB } } + /// + /// Executes a query on a database, returning the first column of the first row of the result set. + /// + /// + /// Database to query + /// Query string with parameters as @0, @1, etc. + /// Parameters to be put in the query + /// + public static T QueryScalar(this IDbConnection olddb, string query, params object[] args) + { + using (var db = olddb.CloneEx()) + { + db.Open(); + using (var com = db.CreateCommand()) + { + com.CommandText = query; + for (int i = 0; i < args.Length; i++) + com.AddParameter("@" + i, args[i]); + + object output = com.ExecuteScalar(); + if (typeof(IConvertible).IsAssignableFrom(output.GetType())) + { + return (T)Convert.ChangeType(output, typeof(T)); + } + + return (T)output; + } + } + } + public static QueryResult QueryReaderDict(this IDbConnection olddb, string query, Dictionary values) { var db = olddb.CloneEx(); @@ -156,10 +185,10 @@ namespace TShockAPI.DB typeof (Int32?), (s, i) => s.IsDBNull(i) ? null : (object)s.GetInt32(i) }, - { + /*{ typeof (Int64), (s, i) => s.GetInt64(i) - }, + },*/ { typeof (Int64?), (s, i) => s.IsDBNull(i) ? null : (object)s.GetInt64(i) @@ -210,12 +239,24 @@ namespace TShockAPI.DB public static T Get(this IDataReader reader, int column) { if (reader.IsDBNull(column)) - return default(T); + return default; if (ReadFuncs.ContainsKey(typeof(T))) return (T)ReadFuncs[typeof(T)](reader, column); - throw new NotImplementedException(); + Type t; + if (typeof(T) != (t = reader.GetFieldType(column))) + { + string columnName = reader.GetName(column); + throw new InvalidCastException($"Received type '{typeof(T).Name}', however column '{columnName}' expects type '{t.Name}'"); + } + + if (reader.IsDBNull(column)) + { + return default; + } + + return (T)reader.GetValue(column); } } diff --git a/TShockAPI/Extensions/StringExt.cs b/TShockAPI/Extensions/StringExt.cs index 2ba5aac3..56f46215 100644 --- a/TShockAPI/Extensions/StringExt.cs +++ b/TShockAPI/Extensions/StringExt.cs @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +using Microsoft.Xna.Framework; using System; namespace TShockAPI @@ -27,5 +28,16 @@ namespace TShockAPI { return String.Format(str, args); } + + /// + /// Wraps the string representation of an object with a Terraria color code for the given color + /// + /// + /// + /// + public static string Color(this object obj, string color) + { + return $"[c/{color}:{obj}]"; + } } -} \ No newline at end of file +} diff --git a/TShockAPI/Rest/RestManager.cs b/TShockAPI/Rest/RestManager.cs index dec8662c..45a3d03d 100644 --- a/TShockAPI/Rest/RestManager.cs +++ b/TShockAPI/Rest/RestManager.cs @@ -638,25 +638,25 @@ namespace TShockAPI if (TShock.Bans.InsertBan(identifier, reason, args.TokenData.Username, startDate, endDate) != null) { TSPlayer player = null; - if (identifier.StartsWith(Ban.Identifiers.IP)) + if (identifier.StartsWith(Identifiers.IP)) { - player = TShock.Players.FirstOrDefault(p => p.IP == identifier.Substring(Ban.Identifiers.IP.Length)); + player = TShock.Players.FirstOrDefault(p => p.IP == identifier.Substring(Identifiers.IP.Length)); } - else if (identifier.StartsWith(Ban.Identifiers.Name)) + else if (identifier.StartsWith(Identifiers.Name)) { //Character names may not necessarily be unique, so kick all matches - foreach (var ply in TShock.Players.Where(p => p.Name == identifier.Substring(Ban.Identifiers.Name.Length))) + foreach (var ply in TShock.Players.Where(p => p.Name == identifier.Substring(Identifiers.Name.Length))) { ply.Kick(reason, true); } } - else if (identifier.StartsWith(Ban.Identifiers.Account)) + else if (identifier.StartsWith(Identifiers.Account)) { - player = TShock.Players.FirstOrDefault(p => p.Account?.Name == identifier.Substring(Ban.Identifiers.Account.Length)); + player = TShock.Players.FirstOrDefault(p => p.Account?.Name == identifier.Substring(Identifiers.Account.Length)); } - else if (identifier.StartsWith(Ban.Identifiers.UUID)) + else if (identifier.StartsWith(Identifiers.UUID)) { - player = TShock.Players.FirstOrDefault(p => p.UUID == identifier.Substring(Ban.Identifiers.UUID.Length)); + player = TShock.Players.FirstOrDefault(p => p.UUID == identifier.Substring(Identifiers.UUID.Length)); } if (player != null) @@ -673,18 +673,23 @@ namespace TShockAPI [Description("Delete an existing ban entry.")] [Route("/v3/bans/destroy")] [Permission(RestPermissions.restmanagebans)] - [Noun("identifier", true, "The identifier of the ban to delete.", typeof(String))] + [Noun("uniqueId", true, "The unique ID of the ban to delete.", typeof(String))] [Noun("fullDelete", false, "Whether or not to completely remove the ban from the system.", typeof(bool))] [Token] private object BanDestroyV3(RestRequestArgs args) { - string identifier = args.Parameters["identifier"]; - if (string.IsNullOrWhiteSpace(identifier)) - return RestMissingParam("identifier"); + string id = args.Parameters["uniqueId"]; + if (string.IsNullOrWhiteSpace(id)) + return RestMissingParam("uniqueId"); + + if (!int.TryParse(id, out int uniqueId)) + { + return RestInvalidParam("uniqueId"); + } bool.TryParse(args.Parameters["fullDelete"], out bool fullDelete); - if (TShock.Bans.RemoveBan(identifier, fullDelete)) + if (TShock.Bans.RemoveBan(uniqueId, fullDelete)) { return RestResponse("Ban removed."); } @@ -695,15 +700,20 @@ namespace TShockAPI [Description("View the details of a specific ban.")] [Route("/v3/bans/read")] [Permission(RestPermissions.restviewbans)] - [Noun("identifier", true, "The identifier to search for.", typeof(String))] + [Noun("uniqueId", true, "The unique ID to search for.", typeof(String))] [Token] private object BanInfoV3(RestRequestArgs args) { - string identifier = args.Parameters["identifier"]; - if (string.IsNullOrWhiteSpace(identifier)) - return RestMissingParam("identifier"); + string id = args.Parameters["uniqueId"]; + if (string.IsNullOrWhiteSpace(id)) + return RestMissingParam("uniqueId"); - Ban ban = TShock.Bans.GetBanByIdentifier(identifier); + if (!int.TryParse(id, out int uniqueId)) + { + return RestInvalidParam("uniqueId"); + } + + Ban ban = TShock.Bans.GetBanById(uniqueId); if (ban == null) { @@ -726,7 +736,7 @@ namespace TShockAPI [Token] private object BanListV3(RestRequestArgs args) { - IEnumerable bans = TShock.Bans.GetAllBans(); + IEnumerable bans = TShock.Bans.Bans.Select(kvp => kvp.Value); var banList = new ArrayList(); foreach (var ban in bans) diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index 4981b784..923dc5af 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -1638,11 +1638,11 @@ namespace TShockAPI return true; if (force || !HasPermission(Permissions.immunetoban)) { - TShock.Bans.InsertBan($"{DB.Ban.Identifiers.IP}{IP}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); - TShock.Bans.InsertBan($"{DB.Ban.Identifiers.IP}{UUID}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); + TShock.Bans.InsertBan($"{Identifiers.IP}{IP}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); + TShock.Bans.InsertBan($"{Identifiers.IP}{UUID}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); if (Account != null) { - TShock.Bans.InsertBan($"{DB.Ban.Identifiers.Account}{Account.Name}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); + TShock.Bans.InsertBan($"{Identifiers.Account}{Account.Name}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue); } Disconnect(string.Format("Banned: {0}", reason)); diff --git a/TShockAPI/TSServerPlayer.cs b/TShockAPI/TSServerPlayer.cs index 97121198..09642f59 100644 --- a/TShockAPI/TSServerPlayer.cs +++ b/TShockAPI/TSServerPlayer.cs @@ -25,6 +25,7 @@ using Terraria.Utilities; using TShockAPI; using TShockAPI.DB; using Terraria.Localization; +using System.Linq; namespace TShockAPI { @@ -41,30 +42,22 @@ namespace TShockAPI public override void SendErrorMessage(string msg) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(msg); - Console.ResetColor(); + SendConsoleMessage(msg, 255, 0, 0); } public override void SendInfoMessage(string msg) { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine(msg); - Console.ResetColor(); + SendConsoleMessage(msg, 255, 250, 170); } public override void SendSuccessMessage(string msg) { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine(msg); - Console.ResetColor(); + SendConsoleMessage(msg, 0, 255, 0); } public override void SendWarningMessage(string msg) { - Console.ForegroundColor = ConsoleColor.DarkRed; - Console.WriteLine(msg); - Console.ResetColor(); + SendConsoleMessage(msg, 139, 0, 0); } public override void SendMessage(string msg, Color color) @@ -74,7 +67,28 @@ namespace TShockAPI public override void SendMessage(string msg, byte red, byte green, byte blue) { - Console.WriteLine(msg); + SendConsoleMessage(msg, red, green, blue); + } + + public void SendConsoleMessage(string msg, byte red, byte green, byte blue) + { + var snippets = Terraria.UI.Chat.ChatManager.ParseMessage(msg, new Color(red, green, blue)); + + foreach (var snippet in snippets) + { + if (snippet.Color != null) + { + Console.ForegroundColor = PickNearbyConsoleColor(snippet.Color); + } + else + { + Console.ForegroundColor = ConsoleColor.Gray; + } + + Console.Write(snippet.Text); + } + Console.WriteLine(); + Console.ResetColor(); } public void SetFullMoon() @@ -179,5 +193,47 @@ namespace TShockAPI All.SendTileSquare((int)coords.X, (int)coords.Y, 3); } } + + + private readonly Dictionary _consoleColorMap = new Dictionary + { + { Color.Red, ConsoleColor.Red }, + { Color.Green, ConsoleColor.Green }, + { Color.Blue, ConsoleColor.Cyan }, + { new Color(255, 250, 170), ConsoleColor.Yellow }, + { new Color(170, 170, 255), ConsoleColor.Cyan }, + { new Color(255, 170, 255), ConsoleColor.Magenta }, + { new Color(170, 255, 170), ConsoleColor.Green }, + { new Color(255, 170, 170), ConsoleColor.Red }, + { new Color(139, 0, 0), ConsoleColor.DarkRed }, // This is the console warning color + { Color.PaleVioletRed, ConsoleColor.Magenta }, // This is the command logging color + { Color.White, ConsoleColor.White } + }; + + private ConsoleColor PickNearbyConsoleColor(Color color) + { + //Grabs an integer difference between two colors in euclidean space + int ColorDiff(Color c1, Color c2) + { + return (int)Math.Sqrt((c1.R - c2.R) * (c1.R - c2.R) + + (c1.G - c2.G) * (c1.G - c2.G) + + (c1.B - c2.B) * (c1.B - c2.B)); + } + + var diffs = _consoleColorMap.Select(kvp => ColorDiff(kvp.Key, color)); + int index = 0; + int min = int.MaxValue; + + for (int i = 0; i < _consoleColorMap.Count; i++) + { + if (diffs.ElementAt(i) < min) + { + index = i; + min = diffs.ElementAt(i); + } + } + + return _consoleColorMap.Values.ElementAt(index); + } } -} \ No newline at end of file +} diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index 8c3b752f..449dfc14 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -478,13 +478,21 @@ namespace TShockAPI args.Player.Account.KnownIps = JsonConvert.SerializeObject(KnownIps, Formatting.Indented); UserAccounts.UpdateLogin(args.Player.Account); - //Check if this user has any recorded bans - var validBan = Bans.GetBansByIdentifiers($"acc:{args.Player.Account.Name}", $"uuid:{args.Player.UUID}").FirstOrDefault(b => Bans.IsValidBan(b)); + //Check if this user has a recorded ban on their account + var ban = Bans.Bans.FirstOrDefault(b => b.Value.Identifier == $"{Identifiers.Account}{args.Player.Account.Name}" && Bans.IsValidBan(b.Value, args.Player)).Value; - //If they do and any are still valid, kick them - if (validBan != null) + //If they do and the ban is still valid, kick them + if (ban != null && !args.Player.HasPermission(Permissions.immunetoban)) { - args.Player.Kick($"You are banned: {validBan.Reason}", true, true); + if (ban.ExpirationDateTime == DateTime.MaxValue) + { + args.Player.Disconnect("You are banned: " + ban.Reason); + } + else + { + TimeSpan ts = ban.ExpirationDateTime - DateTime.UtcNow; + args.Player.Disconnect($"You are banned: {ban.Reason} ({ban.GetPrettyExpirationString()} remaining)"); + } } } @@ -1196,7 +1204,14 @@ namespace TShockAPI return; } - Ban ban = Bans.GetBansByIdentifiers($"name:{player.Name}", $"uuid:{player.UUID}", $"ip:{player.IP}").FirstOrDefault(b => Bans.IsValidBan(b)); + List identifiers = new List + { + $"{Identifiers.UUID}{player.UUID}", + $"{Identifiers.Name}{player.Name}", + $"{Identifiers.IP}{player.IP}" + }; + + Ban ban = Bans.Bans.FirstOrDefault(b => identifiers.Contains(b.Value.Identifier) && Bans.IsValidBan(b.Value, player)).Value; if (ban != null) { @@ -1207,33 +1222,9 @@ namespace TShockAPI else { TimeSpan ts = ban.ExpirationDateTime - DateTime.UtcNow; - int months = ts.Days / 30; - if (months > 0) - { - player.Disconnect(String.Format("You are banned for {0} month{1} and {2} day{3}: {4}", - months, months == 1 ? "" : "s", ts.Days, ts.Days == 1 ? "" : "s", ban.Reason)); - } - else if (ts.Days > 0) - { - player.Disconnect(String.Format("You are banned for {0} day{1} and {2} hour{3}: {4}", - ts.Days, ts.Days == 1 ? "" : "s", ts.Hours, ts.Hours == 1 ? "" : "s", ban.Reason)); - } - else if (ts.Hours > 0) - { - player.Disconnect(String.Format("You are banned for {0} hour{1} and {2} minute{3}: {4}", - ts.Hours, ts.Hours == 1 ? "" : "s", ts.Minutes, ts.Minutes == 1 ? "" : "s", ban.Reason)); - } - else if (ts.Minutes > 0) - { - player.Disconnect(String.Format("You are banned for {0} minute{1} and {2} second{3}: {4}", - ts.Minutes, ts.Minutes == 1 ? "" : "s", ts.Seconds, ts.Seconds == 1 ? "" : "s", ban.Reason)); - } - else - { - player.Disconnect(String.Format("You are banned for {0} second{1}: {2}", - ts.Seconds, ts.Seconds == 1 ? "" : "s", ban.Reason)); - } + player.Disconnect($"You are banned: {ban.Reason} ({ban.GetPrettyExpirationString()} remaining)"); } + args.Handled = true; } } diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index 26089707..7cfa834a 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -226,7 +226,7 @@ - +