Ban rewrite

This commit is contained in:
Chris 2020-11-15 11:05:04 +10:30
parent 5af1f8f76a
commit 56de9f6684
9 changed files with 679 additions and 860 deletions

View file

@ -1267,328 +1267,271 @@ namespace TShockAPI
private static void Ban(CommandArgs args) private static void Ban(CommandArgs args)
{ {
//Ban syntax:
// ban add <target> [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, <target> is assumed to be a player or player index.
// ban del <target>
// Target is expected to be an identifier in the format 'identifier_prefix:identifier'. Eg acc:MyAccountName
// ban list [page]
// Displays a paginated list of bans
// ban details <target>
// 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
void Help()
{
if (args.Parameters.Count > 1)
{
MoreHelp(args.Parameters[1].ToLower());
return;
}
args.Player.SendMessage("TShock Ban Help", Color.White);
args.Player.SendMessage("Available Ban commands:", Color.White);
args.Player.SendMessage("ban [c/FFAAAA:add] <target> [flags]", Color.White);
args.Player.SendMessage("ban [c/FFAAAA:del] <target>", Color.White);
args.Player.SendMessage("ban [c/FFAAAA:list]", Color.White);
args.Player.SendMessage("ban [c/FFAAAA:details] <target>", Color.White);
args.Player.SendMessage("For more info, use [c/AAAAFF:ban help] [c/FFAAAA:command]", Color.White);
}
void MoreHelp(string cmd)
{
switch (cmd)
{
case "add":
args.Player.SendMessage("", Color.White);
args.Player.SendMessage("Ban Add Syntax", Color.White);
args.Player.SendMessage("[c/AAAAFF:ban add] [c/FFAAAA:<target>] [[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);
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);
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("- 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);
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);
break;
default:
args.Player.SendMessage("Unknown ban command. Try 'add', 'del', 'list', or 'details'", Color.White);
break;
}
}
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);
return;
}
bool exactTarget = args.Parameters.Any(p => p == "-e");
bool banAccount = args.Parameters.Any(p => p == "-a");
bool banUuid = args.Parameters.Any(p => p == "-u");
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);
DateTime expiration = DateTime.MaxValue;
if (TShock.Utils.TryParseTime(duration, out int seconds))
{
expiration = DateTime.UtcNow.AddSeconds(seconds);
}
//If no flags were specified, default to account, uuid, and IP
if (!exactTarget && !banAccount && !banUuid && !banName && !banIp)
{
banAccount = banUuid = banIp = true;
}
if (exactTarget)
{
if (TShock.Bans.InsertBan(target, reason ?? "Banned", args.Player.Account.Name, DateTime.UtcNow, expiration) != null)
{
args.Player.SendSuccessMessage("Ban added.");
}
else
{
args.Player.SendErrorMessage("Failed to insert ban. Ban may already exist, or an error occured.");
}
return;
}
var players = TSPlayer.FindByNameOrID(target);
if (players.Count > 1)
{
args.Player.SendMultipleMatchError(players.Select(p => p.Name));
return;
}
if (players.Count < 1)
{
args.Player.SendErrorMessage("Could not find the target specified. Check that you have the correct spelling.");
return;
}
var player = players[0];
var identifiers = new List<string>();
string identifier;
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);
}
}
}
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);
}
}
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);
}
}
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);
}
}
args.Player.SendSuccessMessage("Bans added for identifiers: ", string.Join(", ", identifiers));
}
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);
return;
}
if (TShock.Bans.RemoveBan(target))
{
args.Player.SendSuccessMessage("Ban removed.");
}
else
{
args.Player.SendErrorMessage("Failed to remove ban.");
}
}
void ListBans()
{
int pageNumber;
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out 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);
return;
}
List<Ban> bans = TShock.Bans.GetAllBans();
var nameBans = from ban in bans
select ban.Identifier;
PaginationTools.SendPage(args.Player, pageNumber, PaginationTools.BuildLinesFromTerms(nameBans),
new PaginationTools.Settings
{
HeaderFormat = "Bans ({0}/{1}):",
FooterFormat = "Type {0}ban list {{0}} for more.".SFormat(Specifier),
NothingToDisplayString = "There are currently no bans."
});
}
void BanDetails()
{
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);
return;
}
Ban ban = TShock.Bans.GetBanByIdentifier(target);
if (ban == null)
{
args.Player.SendErrorMessage("No ban found matching the given identifier");
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);
}
string subcmd = args.Parameters.Count == 0 ? "help" : args.Parameters[0].ToLower(); string subcmd = args.Parameters.Count == 0 ? "help" : args.Parameters[0].ToLower();
switch (subcmd) switch (subcmd)
{ {
case "add":
#region Add Ban
{
if (args.Parameters.Count < 2)
{
args.Player.SendErrorMessage("Invalid command. Format: {0}ban add <player> [time] [reason]", Specifier);
args.Player.SendErrorMessage("Example: {0}ban add Shank 10d Hacking and cheating", Specifier);
args.Player.SendErrorMessage("Example: {0}ban add Ash", Specifier);
args.Player.SendErrorMessage("Use the time 0 (zero) for a permanent ban.");
return;
}
// Used only to notify if a ban was successful and who the ban was about
bool success = false;
string targetGeneralizedName = "";
// Effective ban target assignment
List<TSPlayer> players = TSPlayer.FindByNameOrID(args.Parameters[1]);
// Bad case: Players contains more than 1 person so we can't ban them
if (players.Count > 1)
{
//Fail fast
args.Player.SendMultipleMatchError(players.Select(p => p.Name));
return;
}
UserAccount offlineUserAccount = TShock.UserAccounts.GetUserAccountByName(args.Parameters[1]);
// Storage variable to determine if the command executor is the server console
// If it is, we assume they have full control and let them override permission checks
bool callerIsServerConsole = args.Player is TSServerPlayer;
// The ban reason the ban is going to have
string banReason = "Unknown.";
// The default ban length
// 0 is permanent ban, otherwise temp ban
int banLengthInSeconds = 0;
// Figure out if param 2 is a time or 0 or garbage
if (args.Parameters.Count >= 3)
{
bool parsedOkay = false;
if (args.Parameters[2] != "0")
{
parsedOkay = TShock.Utils.TryParseTime(args.Parameters[2], out banLengthInSeconds);
}
else
{
parsedOkay = true;
}
if (!parsedOkay)
{
args.Player.SendErrorMessage("Invalid time format. Example: 10d 5h 3m 2s.");
args.Player.SendErrorMessage("Use 0 (zero) for a permanent ban.");
return;
}
}
// If a reason exists, use the given reason.
if (args.Parameters.Count > 3)
{
banReason = String.Join(" ", args.Parameters.Skip(3));
}
// Good case: Online ban for matching character.
if (players.Count == 1)
{
TSPlayer target = players[0];
if (target.HasPermission(Permissions.immunetoban) && !callerIsServerConsole)
{
args.Player.SendErrorMessage("Permission denied. Target {0} is immune to ban.", target.Name);
return;
}
targetGeneralizedName = target.Name;
success = TShock.Bans.AddBan(target.IP, target.Name, target.UUID, target.Account?.Name ?? "", banReason, false, args.Player.Account.Name,
banLengthInSeconds == 0 ? "" : DateTime.UtcNow.AddSeconds(banLengthInSeconds).ToString("s"));
// Since this is an online ban, we need to dc the player and tell them now.
if (success)
{
if (banLengthInSeconds == 0)
{
target.Disconnect(String.Format("Permanently banned for {0}", banReason));
}
else
{
target.Disconnect(String.Format("Banned for {0} seconds for {1}", banLengthInSeconds, banReason));
}
}
}
// Case: Players & user are invalid, could be IP?
// Note: Order matters. If this method is above the online player check,
// This enables you to ban an IP even if the player exists in the database as a player.
// You'll get two bans for the price of one, in theory, because both IP and user named IP will be banned.
// ??? edge cases are weird, but this is going to happen
// The only way around this is to either segregate off the IP code or do something else.
if (players.Count == 0)
{
// If the target is a valid IP...
string pattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
Regex r = new Regex(pattern, RegexOptions.IgnoreCase);
if (r.IsMatch(args.Parameters[1]))
{
targetGeneralizedName = "IP: " + args.Parameters[1];
success = TShock.Bans.AddBan(args.Parameters[1], "", "", "", banReason,
false, args.Player.Account.Name, banLengthInSeconds == 0 ? "" : DateTime.UtcNow.AddSeconds(banLengthInSeconds).ToString("s"));
if (success && offlineUserAccount != null)
{
args.Player.SendSuccessMessage("Target IP {0} was banned successfully.", targetGeneralizedName);
args.Player.SendErrorMessage("Note: An account named with this IP address also exists.");
args.Player.SendErrorMessage("Note: It will also be banned.");
}
}
else
{
// Apparently there is no way to not IP ban someone
// This means that where we would normally just ban a "character name" here
// We can't because it requires some IP as a primary key.
if (offlineUserAccount == null)
{
args.Player.SendErrorMessage("Unable to ban target {0}.", args.Parameters[1]);
args.Player.SendErrorMessage("Target is not a valid IP address, a valid online player, or a known offline user.");
return;
}
}
}
// Case: Offline ban
if (players.Count == 0 && offlineUserAccount != null)
{
// Catch: we don't know an offline player's last login character name
// This means that we're banning their *user name* on the assumption that
// user name == character name
// (which may not be true)
// This needs to be fixed in a future implementation.
targetGeneralizedName = offlineUserAccount.Name;
if (TShock.Groups.GetGroupByName(offlineUserAccount.Group).HasPermission(Permissions.immunetoban) &&
!callerIsServerConsole)
{
args.Player.SendErrorMessage("Permission denied. Target {0} is immune to ban.", targetGeneralizedName);
return;
}
if (offlineUserAccount.KnownIps == null)
{
args.Player.SendErrorMessage("Unable to ban target {0} because they have no valid IP to ban.", targetGeneralizedName);
return;
}
string lastIP = JsonConvert.DeserializeObject<List<string>>(offlineUserAccount.KnownIps).Last();
success =
TShock.Bans.AddBan(lastIP,
"", offlineUserAccount.UUID, offlineUserAccount.Name, banReason, false, args.Player.Account.Name,
banLengthInSeconds == 0 ? "" : DateTime.UtcNow.AddSeconds(banLengthInSeconds).ToString("s"));
}
if (success)
{
args.Player.SendSuccessMessage("{0} was successfully banned.", targetGeneralizedName);
args.Player.SendInfoMessage("Length: {0}", banLengthInSeconds == 0 ? "Permanent." : banLengthInSeconds + " seconds.");
args.Player.SendInfoMessage("Reason: {0}", banReason);
if (!args.Silent)
{
if (banLengthInSeconds == 0)
{
TSPlayer.All.SendErrorMessage("{0} was permanently banned by {1} for: {2}",
targetGeneralizedName, args.Player.Account.Name, banReason);
}
else
{
TSPlayer.All.SendErrorMessage("{0} was temp banned for {1} seconds by {2} for: {3}",
targetGeneralizedName, banLengthInSeconds, args.Player.Account.Name, banReason);
}
}
}
else
{
args.Player.SendErrorMessage("{0} was NOT banned due to a database error or other system problem.", targetGeneralizedName);
args.Player.SendErrorMessage("If this player is online, they have NOT been kicked.");
args.Player.SendErrorMessage("Check the system logs for details.");
}
return;
}
#endregion
case "del":
#region Delete ban
{
if (args.Parameters.Count != 2)
{
args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}ban del <player>", Specifier);
return;
}
string plStr = args.Parameters[1];
Ban ban = TShock.Bans.GetBanByName(plStr, false);
if (ban != null)
{
if (TShock.Bans.RemoveBan(ban.Name, true))
args.Player.SendSuccessMessage("Unbanned {0} ({1}).", ban.Name, ban.IP);
else
args.Player.SendErrorMessage("Failed to unban {0} ({1}), check logs.", ban.Name, ban.IP);
}
else
args.Player.SendErrorMessage("No bans for {0} exist.", plStr);
}
#endregion
return;
case "delip":
#region Delete IP ban
{
if (args.Parameters.Count != 2)
{
args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}ban delip <ip>", Specifier);
return;
}
string ip = args.Parameters[1];
Ban ban = TShock.Bans.GetBanByIp(ip);
if (ban != null)
{
if (TShock.Bans.RemoveBan(ban.IP, false))
args.Player.SendSuccessMessage("Unbanned IP {0} ({1}).", ban.IP, ban.Name);
else
args.Player.SendErrorMessage("Failed to unban IP {0} ({1}), check logs.", ban.IP, ban.Name);
}
else
args.Player.SendErrorMessage("IP {0} is not banned.", ip);
}
#endregion
return;
case "help": case "help":
#region Help Help();
{ break;
int pageNumber;
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out pageNumber))
return;
var lines = new List<string> case "add":
{ AddBan();
"add <target> <time> [reason] - Bans a player or user account if the player is not online.", break;
"del <player> - Unbans a player.",
"delip <ip> - Unbans an IP.", case "del":
"list [page] - Lists all player bans.", DelBan();
"listip [page] - Lists all IP bans." break;
};
PaginationTools.SendPage(args.Player, pageNumber, lines,
new PaginationTools.Settings
{
HeaderFormat = "Ban Sub-Commands ({0}/{1}):",
FooterFormat = "Type {0}ban help {{0}} for more sub-commands.".SFormat(Specifier)
}
);
}
#endregion
return;
case "list": case "list":
#region List bans ListBans();
{ break;
int pageNumber;
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out pageNumber))
{
return;
}
List<Ban> bans = TShock.Bans.GetBans(); case "details":
BanDetails();
break;
var nameBans = from ban in bans
where !String.IsNullOrEmpty(ban.Name)
select ban.Name;
PaginationTools.SendPage(args.Player, pageNumber, PaginationTools.BuildLinesFromTerms(nameBans),
new PaginationTools.Settings
{
HeaderFormat = "Bans ({0}/{1}):",
FooterFormat = "Type {0}ban list {{0}} for more.".SFormat(Specifier),
NothingToDisplayString = "There are currently no bans."
});
}
#endregion
return;
case "listip":
#region List IP bans
{
int pageNumber;
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out pageNumber))
{
return;
}
List<Ban> bans = TShock.Bans.GetBans();
var ipBans = from ban in bans
where String.IsNullOrEmpty(ban.Name)
select ban.IP;
PaginationTools.SendPage(args.Player, pageNumber, PaginationTools.BuildLinesFromTerms(ipBans),
new PaginationTools.Settings
{
HeaderFormat = "IP Bans ({0}/{1}):",
FooterFormat = "Type {0}ban listip {{0}} for more.".SFormat(Specifier),
NothingToDisplayString = "There are currently no IP bans."
});
}
#endregion
return;
default: default:
args.Player.SendErrorMessage("Invalid subcommand! Type {0}ban help for more information.", Specifier); break;
return;
} }
} }

View file

@ -31,6 +31,15 @@ namespace TShockAPI.DB
{ {
private IDbConnection database; private IDbConnection database;
/// <summary>
/// Event invoked when a ban check occurs
/// </summary>
public static event EventHandler<BanEventArgs> OnBanCheck;
/// <summary>
/// Event invoked when a ban is added
/// </summary>
public static event EventHandler<BanEventArgs> OnBanAdded;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TShockAPI.DB.BanManager"/> class. /// Initializes a new instance of the <see cref="TShockAPI.DB.BanManager"/> class.
/// </summary> /// </summary>
@ -39,15 +48,12 @@ namespace TShockAPI.DB
{ {
database = db; database = db;
var table = new SqlTable("Bans", var table = new SqlTable("PlayerBans",
new SqlColumn("IP", MySqlDbType.String, 16) { Primary = true }, new SqlColumn("Identifier", MySqlDbType.Text) { Primary = true, Unique = true },
new SqlColumn("Name", MySqlDbType.Text),
new SqlColumn("UUID", MySqlDbType.Text),
new SqlColumn("Reason", MySqlDbType.Text), new SqlColumn("Reason", MySqlDbType.Text),
new SqlColumn("BanningUser", MySqlDbType.Text), new SqlColumn("BanningUser", MySqlDbType.Text),
new SqlColumn("Date", MySqlDbType.Text), new SqlColumn("Date", MySqlDbType.Text),
new SqlColumn("Expiration", MySqlDbType.Text), new SqlColumn("Expiration", MySqlDbType.Text)
new SqlColumn("AccountName", MySqlDbType.Text)
); );
var creator = new SqlTableCreator(db, var creator = new SqlTableCreator(db,
db.GetSqlType() == SqlType.Sqlite db.GetSqlType() == SqlType.Sqlite
@ -62,55 +68,204 @@ namespace TShockAPI.DB
System.Console.WriteLine("Possible problem with your database - is Sqlite3.dll present?"); System.Console.WriteLine("Possible problem with your database - is Sqlite3.dll present?");
throw new Exception("Could not find a database library (probably Sqlite3.dll)"); throw new Exception("Could not find a database library (probably Sqlite3.dll)");
} }
OnBanCheck += CheckBanValid;
} }
/// <summary> /// <summary>
/// Gets a ban by IP. /// Converts bans from the old ban system to the new.
/// </summary> /// </summary>
/// <param name="ip">The IP.</param> public void ConvertBans()
/// <returns>The ban.</returns>
public Ban GetBanByIp(string ip)
{ {
try using (var reader = database.QueryReader("SELECT * FROM Bans"))
{ {
using (var reader = database.QueryReader("SELECT * FROM Bans WHERE IP=@0", ip)) while (reader.Read())
{ {
if (reader.Read()) var ip = reader.Get<string>("IP");
return new Ban(reader.Get<string>("IP"), reader.Get<string>("Name"), reader.Get<string>("UUID"), reader.Get<string>("AccountName"), reader.Get<string>("Reason"), reader.Get<string>("BanningUser"), reader.Get<string>("Date"), reader.Get<string>("Expiration")); var uuid = reader.Get<string>("UUID");
var account = reader.Get<string>("AccountName");
var reason = reader.Get<string>("Reason");
var banningUser = reader.Get<string>("BanningUser");
var date = reader.Get<string>("Date");
var expiration = reader.Get<string>("Expiration");
if (!string.IsNullOrWhiteSpace(ip))
{
InsertBan($"{Ban.Identifiers.IP}{ip}", reason, banningUser, date, expiration);
}
if (!string.IsNullOrWhiteSpace(account))
{
InsertBan($"{Ban.Identifiers.Account}{account}", reason, banningUser, date, expiration);
}
if (!string.IsNullOrWhiteSpace(uuid))
{
InsertBan($"{Ban.Identifiers.UUID}{uuid}", reason, banningUser, date, expiration);
}
} }
} }
catch (Exception ex) }
/// <summary>
/// Determines whether or not a ban is valid
/// </summary>
/// <param name="ban"></param>
/// <returns></returns>
public bool IsValidBan(Ban ban)
{
BanEventArgs args = new BanEventArgs { Ban = ban };
OnBanCheck?.Invoke(this, args);
return args.Valid;
}
internal void CheckBanValid(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;
}
/// <summary>
/// Adds a new ban for the given identifier. If the addition succeeds, returns a ban object with the ban details. Else returns null
/// </summary>
/// <param name="identifier"></param>
/// <param name="reason"></param>
/// <param name="banningUser"></param>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <returns></returns>
public Ban InsertBan(string identifier, string reason, string banningUser, DateTime fromDate, DateTime toDate)
=> InsertBan(identifier, reason, banningUser, fromDate.ToString("s"), toDate.ToString("s"));
/// <summary>
/// Adds a new ban for the given identifier. If the addition succeeds, returns a ban object with the ban details. Else returns null
/// </summary>
/// <param name="identifier"></param>
/// <param name="reason"></param>
/// <param name="banningUser"></param>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <returns></returns>
public Ban InsertBan(string identifier, string reason, string banningUser, string fromDate, string toDate)
{
Ban b = new Ban(identifier, reason, banningUser, fromDate, toDate);
BanEventArgs args = new BanEventArgs { Ban = b };
OnBanAdded?.Invoke(this, args);
if (!args.Valid)
{ {
TShock.Log.Error(ex.ToString()); return null;
} }
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;
}
/// <summary>
/// Attempts to remove a ban. Returns true if the ban was removed or expired. False if the ban could not be removed or expired
/// </summary>
/// <param name="identifier"></param>
/// <param name="fullDelete">If true, deletes the ban from the database. If false, marks the expiration time as now, rendering the ban expired. Defaults to false</param>
/// <returns></returns>
public bool RemoveBan(string identifier, bool fullDelete = false)
{
int rowsModified;
if (fullDelete)
{
rowsModified = database.Query("DELETE FROM PlayerBans WHERE Identifier=@0", identifier);
}
else
{
rowsModified = database.Query("UPDATE PlayerBans SET Expiration=@0 WHERE Identifier=@1", DateTime.UtcNow.ToString("s"), identifier);
}
return rowsModified > 0;
}
/// <summary>
/// Retrieves a ban for a given identifier, or null if no matches are found
/// </summary>
/// <param name="identifier"></param>
/// <returns></returns>
public Ban GetBanByIdentifier(string identifier)
{
using (var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE Identifier=@0", identifier))
{
if (reader.Read())
{
var id = reader.Get<string>("Identifier");
var reason = reader.Get<string>("Reason");
var banningUser = reader.Get<string>("BanningUser");
var date = reader.Get<string>("Date");
var expiration = reader.Get<string>("Expiration");
return new Ban(id, reason, banningUser, date, expiration);
}
}
return null; return null;
} }
/// <summary>
/// Retrieves an enumerable of bans for a given set of identifiers
/// </summary>
/// <param name="identifiers"></param>
/// <returns></returns>
public IEnumerable<Ban> GetBansByIdentifiers(params string[] identifiers)
{
//Generate a sequence of '@0, @1, @2, ... etc'
var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Count()).Select(p => $"@{p}"));
using (var reader = database.QueryReader($"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})", identifiers))
{
while (reader.Read())
{
var id = reader.Get<string>("Identifier");
var reason = reader.Get<string>("Reason");
var banningUser = reader.Get<string>("BanningUser");
var date = reader.Get<string>("Date");
var expiration = reader.Get<string>("Expiration");
yield return new Ban(id, reason, banningUser, date, expiration);
}
}
}
/// <summary> /// <summary>
/// Gets a list of bans sorted by their addition date from newest to oldest /// Gets a list of bans sorted by their addition date from newest to oldest
/// </summary> /// </summary>
public List<Ban> GetBans() => GetSortedBans(BanSortMethod.AddedNewestToOldest).ToList(); public List<Ban> GetAllBans() => GetAllBansSorted(BanSortMethod.AddedNewestToOldest).ToList();
/// <summary> /// <summary>
/// Retrieves an enumerable of <see cref="Ban"/> objects, sorted using the provided sort method /// Retrieves an enumerable of <see cref="Ban"/> objects, sorted using the provided sort method
/// </summary> /// </summary>
/// <param name="sortMethod"></param> /// <param name="sortMethod"></param>
/// <returns></returns> /// <returns></returns>
public IEnumerable<Ban> GetSortedBans(BanSortMethod sortMethod) public IEnumerable<Ban> GetAllBansSorted(BanSortMethod sortMethod)
{ {
List<Ban> banlist = new List<Ban>(); List<Ban> banlist = new List<Ban>();
try try
{ {
using (var reader = database.QueryReader("SELECT * FROM Bans")) var orderBy = SortToOrderByMap[sortMethod];
using (var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {orderBy}"))
{ {
while (reader.Read()) while (reader.Read())
{ {
banlist.Add(new Ban(reader.Get<string>("IP"), reader.Get<string>("Name"), reader.Get<string>("UUID"), reader.Get<string>("AccountName"), reader.Get<string>("Reason"), reader.Get<string>("BanningUser"), reader.Get<string>("Date"), reader.Get<string>("Expiration"))); var identifier = reader.Get<string>("Identifier");
} var reason = reader.Get<string>("Reason");
var banningUser = reader.Get<string>("BanningUser");
var date = reader.Get<string>("Date");
var expiration = reader.Get<string>("Expiration");
banlist.Sort(new BanComparer(sortMethod)); var ban = new Ban(identifier, reason, banningUser, date, expiration);
return banlist; banlist.Add(ban);
}
} }
return banlist;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -121,172 +276,14 @@ namespace TShockAPI.DB
} }
/// <summary> /// <summary>
/// Gets a ban by name. /// Removes all bans from the database
/// </summary>
/// <param name="name">The name.</param>
/// <param name="casesensitive">Whether to check with case sensitivity.</param>
/// <returns>The ban.</returns>
public Ban GetBanByName(string name, bool casesensitive = true)
{
try
{
var namecol = casesensitive ? "Name" : "UPPER(Name)";
if (!casesensitive)
name = name.ToUpper();
using (var reader = database.QueryReader("SELECT * FROM Bans WHERE " + namecol + "=@0", name))
{
if (reader.Read())
return new Ban(reader.Get<string>("IP"), reader.Get<string>("Name"), reader.Get<string>("UUID"), reader.Get<string>("AccountName"), reader.Get<string>("Reason"), reader.Get<string>("BanningUser"), reader.Get<string>("Date"), reader.Get<string>("Expiration"));
}
}
catch (Exception ex)
{
TShock.Log.Error(ex.ToString());
}
return null;
}
/// <summary>
/// Gets a ban by account name (not player/character name).
/// </summary>
/// <param name="name">The name.</param>
/// <param name="casesensitive">Whether to check with case sensitivity.</param>
/// <returns>The ban.</returns>
public Ban GetBanByAccountName(string name, bool casesensitive = false)
{
try
{
var namecol = casesensitive ? "AccountName" : "UPPER(AccountName)";
if (!casesensitive)
name = name.ToUpper();
using (var reader = database.QueryReader("SELECT * FROM Bans WHERE " + namecol + "=@0", name))
{
if (reader.Read())
return new Ban(reader.Get<string>("IP"), reader.Get<string>("Name"), reader.Get<string>("UUID"), reader.Get<string>("AccountName"), reader.Get<string>("Reason"), reader.Get<string>("BanningUser"), reader.Get<string>("Date"), reader.Get<string>("Expiration"));
}
}
catch (Exception ex)
{
TShock.Log.Error(ex.ToString());
}
return null;
}
/// <summary>
/// Gets a ban by UUID.
/// </summary>
/// <param name="uuid">The UUID.</param>
/// <returns>The ban.</returns>
public Ban GetBanByUUID(string uuid)
{
try
{
using (var reader = database.QueryReader("SELECT * FROM Bans WHERE UUID=@0", uuid))
{
if (reader.Read())
return new Ban(reader.Get<string>("IP"), reader.Get<string>("Name"), reader.Get<string>("UUID"), reader.Get<string>("AccountName"), reader.Get<string>("Reason"), reader.Get<string>("BanningUser"), reader.Get<string>("Date"), reader.Get<string>("Expiration"));
}
}
catch (Exception ex)
{
TShock.Log.Error(ex.ToString());
}
return null;
}
/// <summary>
/// Adds a ban.
/// </summary>
/// <returns><c>true</c>, if ban was added, <c>false</c> otherwise.</returns>
/// <param name="ip">Ip.</param>
/// <param name="name">Name.</param>
/// <param name="uuid">UUID.</param>
/// <param name="reason">Reason.</param>
/// <param name="exceptions">If set to <c>true</c> enable throwing exceptions.</param>
/// <param name="banner">Banner.</param>
/// <param name="expiration">Expiration date.</param>
public bool AddBan(string ip, string name = "", string uuid = "", string accountName = "", string reason = "", bool exceptions = false, string banner = "", string expiration = "")
{
try
{
/*
* If the ban already exists, update its entry with the new date
* and expiration details.
*/
if (GetBanByIp(ip) != null)
{
return database.Query("UPDATE Bans SET Date = @0, Expiration = @1 WHERE IP = @2", DateTime.UtcNow.ToString("s"), expiration, ip) == 1;
}
else
{
return database.Query("INSERT INTO Bans (IP, Name, UUID, Reason, BanningUser, Date, Expiration, AccountName) VALUES (@0, @1, @2, @3, @4, @5, @6, @7);", ip, name, uuid, reason, banner, DateTime.UtcNow.ToString("s"), expiration, accountName) != 0;
}
}
catch (Exception ex)
{
if (exceptions)
throw ex;
TShock.Log.Error(ex.ToString());
}
return false;
}
/// <summary>
/// Removes a ban.
/// </summary>
/// <returns><c>true</c>, if ban was removed, <c>false</c> otherwise.</returns>
/// <param name="match">Match.</param>
/// <param name="byName">If set to <c>true</c> by name.</param>
/// <param name="casesensitive">If set to <c>true</c> casesensitive.</param>
/// <param name="exceptions">If set to <c>true</c> exceptions.</param>
public bool RemoveBan(string match, bool byName = false, bool casesensitive = true, bool exceptions = false)
{
try
{
if (!byName)
return database.Query("DELETE FROM Bans WHERE IP=@0", match) != 0;
var namecol = casesensitive ? "Name" : "UPPER(Name)";
return database.Query("DELETE FROM Bans WHERE " + namecol + "=@0", casesensitive ? match : match.ToUpper()) != 0;
}
catch (Exception ex)
{
if (exceptions)
throw ex;
TShock.Log.Error(ex.ToString());
}
return false;
}
/// <summary>
/// Removes a ban by account name (not character/player name).
/// </summary>
/// <returns><c>true</c>, if ban was removed, <c>false</c> otherwise.</returns>
/// <param name="match">Match.</param>
/// <param name="casesensitive">If set to <c>true</c> casesensitive.</param>
public bool RemoveBanByAccountName(string match, bool casesensitive = true)
{
try
{
var namecol = casesensitive ? "AccountName" : "UPPER(AccountName)";
return database.Query("DELETE FROM Bans WHERE " + namecol + "=@0", casesensitive ? match : match.ToUpper()) != 0;
}
catch (Exception ex)
{
TShock.Log.Error(ex.ToString());
}
return false;
}
/// <summary>
/// Clears bans.
/// </summary> /// </summary>
/// <returns><c>true</c>, if bans were cleared, <c>false</c> otherwise.</returns> /// <returns><c>true</c>, if bans were cleared, <c>false</c> otherwise.</returns>
public bool ClearBans() public bool ClearBans()
{ {
try try
{ {
return database.Query("DELETE FROM Bans") != 0; return database.Query("DELETE FROM PlayerBans") != 0;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -295,19 +292,13 @@ namespace TShockAPI.DB
return false; return false;
} }
/// <summary>Removes a ban if it has expired.</summary> internal Dictionary<BanSortMethod, string> SortToOrderByMap = new Dictionary<BanSortMethod, string>
/// <param name="ban">The candidate ban to check.</param>
/// <returns>If the ban has been removed.</returns>
public bool RemoveBanIfExpired(Ban ban)
{ {
if (!string.IsNullOrWhiteSpace(ban.Expiration) && (ban.ExpirationDateTime != null) && (DateTime.UtcNow >= ban.ExpirationDateTime)) { BanSortMethod.AddedNewestToOldest, "Date DESC" },
{ { BanSortMethod.AddedOldestToNewest, "Date ASC" },
RemoveBan(ban.IP, false, false, false); { BanSortMethod.ExpirationSoonestToLatest, "Expiration ASC" },
return true; { BanSortMethod.ExpirationLatestToSoonest, "Expiration DESC" }
} };
return false;
}
} }
/// <summary> /// <summary>
@ -334,88 +325,18 @@ namespace TShockAPI.DB
} }
/// <summary> /// <summary>
/// An <see cref="IComparer{Ban}"/> used for sorting an enumerable of bans /// Event args used when a ban check is invoked, or a new ban is added
/// </summary> /// </summary>
public class BanComparer : IComparer<Ban> public class BanEventArgs : EventArgs
{ {
private BanSortMethod _method;
/// <summary> /// <summary>
/// Generates a new <see cref="BanComparer"/> using the given <see cref="BanSortMethod"/> /// The ban being checked or added
/// </summary> /// </summary>
/// <param name="method"></param> public Ban Ban { get; set; }
public BanComparer(BanSortMethod method)
{
_method = method;
}
private int CompareDateTimes(DateTime? x, DateTime? y)
{
if (x == null)
{
if (y == null)
{
//If both bans have no BanDateTime they're considered equal
return 0;
}
//If we're sorting by a newest to oldest method, a null value will come after the valid value.
return _method == BanSortMethod.AddedNewestToOldest || _method == BanSortMethod.ExpirationSoonestToLatest ? 1 : -1;
}
if (y == null)
{
return _method == BanSortMethod.AddedNewestToOldest || _method == BanSortMethod.ExpirationSoonestToLatest ? -1 : 1;
}
//Newest to oldest sorting uses x compared to y. Oldest to newest uses y compared to x
return _method == BanSortMethod.AddedNewestToOldest || _method == BanSortMethod.ExpirationSoonestToLatest ? x.Value.CompareTo(y.Value)
: y.Value.CompareTo(x.Value);
}
/// <summary> /// <summary>
/// Compares two ban objects /// Whether or not the operation is valid
/// </summary> /// </summary>
/// <param name="x"></param> public bool Valid { get; set; } = true;
/// <param name="y"></param>
/// <returns>1 if x is less than y, 0 if x is equal to y, -1 if x is greater than y</returns>
public int Compare(Ban x, Ban y)
{
if (x == null)
{
if (y == null)
{
return 0;
}
//If Ban y is null and Ban x is not, and we're sorting from newest to oldest, x goes before y. Else y goes before x
return _method == BanSortMethod.AddedNewestToOldest || _method == BanSortMethod.ExpirationSoonestToLatest ? -1 : 1;
}
if (x == null)
{
if (y == null)
{
return 0;
}
//If Ban y is null and Ban x is not, and we're sorting from newest to oldest, x goes before y. Else y goes before x
return _method == BanSortMethod.AddedNewestToOldest || _method == BanSortMethod.ExpirationSoonestToLatest ? -1 : 1;
}
switch (_method)
{
case BanSortMethod.AddedNewestToOldest:
case BanSortMethod.AddedOldestToNewest:
return CompareDateTimes(x.BanDateTime, y.BanDateTime);
case BanSortMethod.ExpirationSoonestToLatest:
case BanSortMethod.ExpirationLatestToSoonest:
return CompareDateTimes(x.ExpirationDateTime, y.ExpirationDateTime);
default:
return 0;
}
}
} }
/// <summary> /// <summary>
@ -424,28 +345,32 @@ namespace TShockAPI.DB
public class Ban public class Ban
{ {
/// <summary> /// <summary>
/// Gets or sets the IP address of the ban entry. /// Contains constants for different identifier types known to TShock
/// </summary> /// </summary>
/// <value>The IP Address</value> public class Identifiers
public string IP { get; set; } {
/// <summary>
/// IP identifier prefix constant
/// </summary>
public const string IP = "ip:";
/// <summary>
/// UUID identifier prefix constant
/// </summary>
public const string UUID = "uuid:";
/// <summary>
/// Player name identifier prefix constant
/// </summary>
public const string Name = "name:";
/// <summary>
/// User account identifier prefix constant
/// </summary>
public const string Account = "acc:";
}
/// <summary> /// <summary>
/// Gets or sets the name. /// An identifiable piece of information to ban
/// </summary> /// </summary>
/// <value>The name.</value> public string Identifier { get; set; }
public string Name { get; set; }
/// <summary>
/// Gets or sets the Client UUID of the ban
/// </summary>
/// <value>The UUID</value>
public string UUID { get; set; }
/// <summary>
/// Gets or sets the account name of the ban
/// </summary>
/// <value>The account name</value>
public String AccountName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the ban reason. /// Gets or sets the ban reason.
@ -460,73 +385,46 @@ namespace TShockAPI.DB
public string BanningUser { get; set; } public string BanningUser { get; set; }
/// <summary> /// <summary>
/// Gets or sets the UTC date of when the ban is to take effect /// DateTime from which the ban will take effect
/// </summary> /// </summary>
/// <value>The date, which must be in UTC</value> public DateTime BanDateTime { get; }
public string Date { get; set; }
/// <summary> /// <summary>
/// Gets the <see cref="System.DateTime"/> object representation of the <see cref="Date"/> string. /// DateTime at which the ban will end
/// </summary> /// </summary>
public DateTime? BanDateTime { get; } public DateTime ExpirationDateTime { get; }
/// <summary>
/// Gets or sets the expiration date, in which the ban shall be lifted
/// </summary>
/// <value>The expiration.</value>
public string Expiration { get; set; }
/// <summary>
/// Gets the <see cref="System.DateTime"/> object representation of the <see cref="Expiration"/> string.
/// </summary>
public DateTime? ExpirationDateTime { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TShockAPI.DB.Ban"/> class. /// Initializes a new instance of the <see cref="TShockAPI.DB.Ban"/> class.
/// </summary> /// </summary>
/// <param name="ip">Ip.</param> /// <param name="identifier">Identifier to apply the ban to</param>
/// <param name="name">Name.</param> /// <param name="reason">Reason for the ban</param>
/// <param name="uuid">UUID.</param> /// <param name="banningUser">Account name that executed the ban</param>
/// <param name="reason">Reason.</param> /// <param name="start">Ban start datetime</param>
/// <param name="banner">Banner.</param> /// <param name="end">Ban end datetime</param>
/// <param name="date">UTC ban date.</param> public Ban(string identifier, string reason, string banningUser, string start, string end)
/// <param name="exp">Expiration time</param>
public Ban(string ip, string name, string uuid, string accountName, string reason, string banner, string date, string exp)
{ {
IP = ip; Identifier = identifier;
Name = name;
UUID = uuid;
AccountName = accountName;
Reason = reason; Reason = reason;
BanningUser = banner; BanningUser = banningUser;
Date = date;
Expiration = exp;
DateTime d; if (DateTime.TryParse(start, out DateTime d))
DateTime e;
if (DateTime.TryParse(Date, out d))
{ {
BanDateTime = d; BanDateTime = d;
} }
if (DateTime.TryParse(Expiration, out e)) else
{
BanDateTime = DateTime.UtcNow;
}
if (DateTime.TryParse(end, out DateTime e))
{ {
ExpirationDateTime = e; ExpirationDateTime = e;
} }
} else
{
/// <summary> ExpirationDateTime = DateTime.MaxValue;
/// Initializes a new instance of the <see cref="TShockAPI.DB.Ban"/> class. }
/// </summary>
public Ban()
{
IP = string.Empty;
Name = string.Empty;
UUID = string.Empty;
AccountName = string.Empty;
Reason = string.Empty;
BanningUser = string.Empty;
Date = string.Empty;
Expiration = string.Empty;
} }
} }
} }

View file

@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace TShockAPI namespace TShockAPI
{ {
@ -31,5 +32,25 @@ namespace TShockAPI
foreach (T item in source) foreach (T item in source)
action(item); action(item);
} }
/// <summary>
/// Attempts to retrieve the value at the given index from the enumerable
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="enumerable"></param>
/// <param name="index"></param>
/// <param name="value"></param>
/// <returns></returns>
public static bool TryGetValue<T>(this IEnumerable<T> enumerable, int index, out T value)
{
if (index < enumerable.Count())
{
value = enumerable.ElementAt(index);
return true;
}
value = default;
return false;
}
} }
} }

View file

@ -3592,7 +3592,7 @@ namespace TShockAPI
return true; return true;
} }
return false; return true;
} }
private static bool HandleKillPortal(GetDataHandlerArgs args) private static bool HandleKillPortal(GetDataHandlerArgs args)

View file

@ -155,7 +155,7 @@ namespace TShockAPI
/// <summary> /// <summary>
/// Creates a new instance of <see cref="Token"/> /// Creates a new instance of <see cref="Token"/>
/// </summary> /// </summary>
public Token() : base("token", true, "The REST authentication token.", typeof(String)){} public Token() : base("token", true, "The REST authentication token.", typeof(String)) { }
} }
/// <summary> /// <summary>
@ -216,9 +216,13 @@ namespace TShockAPI
Rest.RegisterRedirect("/users/update", "/v2/users/update"); Rest.RegisterRedirect("/users/update", "/v2/users/update");
//ban commands //ban commands
Rest.RegisterRedirect("/bans/list", "/v2/bans/list"); Rest.RegisterRedirect("/bans/create", "/v3/bans/create");
Rest.RegisterRedirect("/bans/read", "/v2/bans/read"); Rest.RegisterRedirect("/bans/list", "/v3/bans/list");
Rest.RegisterRedirect("/bans/destroy", "/v2/bans/destroy"); Rest.RegisterRedirect("/bans/read", "/v3/bans/read");
Rest.RegisterRedirect("/bans/destroy", "/v3/bans/destroy");
Rest.RegisterRedirect("/v2/bans/list", "/v3/bans/list");
Rest.RegisterRedirect("/v2/bans/read", "/v3/bans/read");
Rest.RegisterRedirect("/v2/bans/destroy", "/v3/bans/destroy");
//world commands //world commands
Rest.RegisterRedirect("/world/bloodmoon", "v3/world/bloodmoon"); Rest.RegisterRedirect("/world/bloodmoon", "v3/world/bloodmoon");
@ -258,10 +262,10 @@ namespace TShockAPI
Rest.Register(new SecureRestCommand("/v2/users/update", UserUpdateV2, RestPermissions.restmanageusers) { DoLog = false }); Rest.Register(new SecureRestCommand("/v2/users/update", UserUpdateV2, RestPermissions.restmanageusers) { DoLog = false });
// Ban Commands // Ban Commands
Rest.Register(new SecureRestCommand("/bans/create", BanCreate, RestPermissions.restmanagebans)); Rest.Register(new SecureRestCommand("/v3/bans/create", BanCreateV3, RestPermissions.restban, RestPermissions.restmanagebans));
Rest.Register(new SecureRestCommand("/v2/bans/list", BanListV2, RestPermissions.restviewbans)); Rest.Register(new SecureRestCommand("/v3/bans/list", BanListV3, RestPermissions.restviewbans));
Rest.Register(new SecureRestCommand("/v2/bans/read", BanInfoV2, RestPermissions.restviewbans)); Rest.Register(new SecureRestCommand("/v3/bans/read", BanInfoV3, RestPermissions.restviewbans));
Rest.Register(new SecureRestCommand("/v2/bans/destroy", BanDestroyV2, RestPermissions.restmanagebans)); Rest.Register(new SecureRestCommand("/v3/bans/destroy", BanDestroyV3, RestPermissions.restmanagebans));
// World Commands // World Commands
Rest.Register(new SecureRestCommand("/world/read", WorldRead)); Rest.Register(new SecureRestCommand("/world/read", WorldRead));
@ -279,7 +283,6 @@ namespace TShockAPI
Rest.Register(new SecureRestCommand("/v3/players/read", PlayerReadV3, RestPermissions.restuserinfo)); Rest.Register(new SecureRestCommand("/v3/players/read", PlayerReadV3, RestPermissions.restuserinfo));
Rest.Register(new SecureRestCommand("/v4/players/read", PlayerReadV4, RestPermissions.restuserinfo)); Rest.Register(new SecureRestCommand("/v4/players/read", PlayerReadV4, RestPermissions.restuserinfo));
Rest.Register(new SecureRestCommand("/v2/players/kick", PlayerKickV2, RestPermissions.restkick)); Rest.Register(new SecureRestCommand("/v2/players/kick", PlayerKickV2, RestPermissions.restkick));
Rest.Register(new SecureRestCommand("/v2/players/ban", PlayerBanV2, RestPermissions.restban, RestPermissions.restmanagebans));
Rest.Register(new SecureRestCommand("/v2/players/kill", PlayerKill, RestPermissions.restkill)); Rest.Register(new SecureRestCommand("/v2/players/kill", PlayerKill, RestPermissions.restkill));
Rest.Register(new SecureRestCommand("/v2/players/mute", PlayerMute, RestPermissions.restmute)); Rest.Register(new SecureRestCommand("/v2/players/mute", PlayerMute, RestPermissions.restmute));
Rest.Register(new SecureRestCommand("/v2/players/unmute", PlayerUnMute, RestPermissions.restmute)); Rest.Register(new SecureRestCommand("/v2/players/unmute", PlayerUnMute, RestPermissions.restmute));
@ -420,7 +423,7 @@ namespace TShockAPI
if (GetBool(args.Parameters["rules"], false)) if (GetBool(args.Parameters["rules"], false))
{ {
var rules = new Dictionary<string,object>(); var rules = new Dictionary<string, object>();
rules.Add("AutoSave", TShock.Config.AutoSave); rules.Add("AutoSave", TShock.Config.AutoSave);
rules.Add("DisableBuild", TShock.Config.DisableBuild); rules.Add("DisableBuild", TShock.Config.DisableBuild);
rules.Add("DisableClownBombs", TShock.Config.DisableClownBombs); rules.Add("DisableClownBombs", TShock.Config.DisableClownBombs);
@ -492,8 +495,8 @@ namespace TShockAPI
return RestMissingParam("user"); return RestMissingParam("user");
var group = args.Parameters["group"]; var group = args.Parameters["group"];
if (string.IsNullOrWhiteSpace(group)) if (string.IsNullOrWhiteSpace(group))
group = TShock.Config.DefaultRegistrationGroupName; group = TShock.Config.DefaultRegistrationGroupName;
var password = args.Parameters["password"]; var password = args.Parameters["password"];
if (string.IsNullOrWhiteSpace(password)) if (string.IsNullOrWhiteSpace(password))
@ -609,115 +612,141 @@ namespace TShockAPI
#region Rest Ban Methods #region Rest Ban Methods
[Description("Create a new ban entry.")] [Description("Create a new ban entry.")]
[Route("/bans/create")] [Route("/v3/bans/create")]
[Permission(RestPermissions.restmanagebans)] [Permission(RestPermissions.restmanagebans)]
[Noun("ip", false, "The IP to ban, at least this or name must be specified.", typeof(String))] [Noun("identifier", true, "The identifier to ban.", typeof(String))]
[Noun("name", false, "The name to ban, at least this or ip must be specified.", typeof(String))]
[Noun("reason", false, "The reason to assign to the ban.", typeof(String))] [Noun("reason", false, "The reason to assign to the ban.", typeof(String))]
[Noun("start", false, "The datetime at which the ban should start.", typeof(String))]
[Noun("end", false, "The datetime at which the ban should end.", typeof(String))]
[Token] [Token]
private object BanCreate(RestRequestArgs args) private object BanCreateV3(RestRequestArgs args)
{ {
var ip = args.Parameters["ip"]; string identifier = args.Parameters["identifier"];
var name = args.Parameters["name"]; if (string.IsNullOrWhiteSpace(identifier))
return RestMissingParam("identifier");
if (string.IsNullOrWhiteSpace(ip) && string.IsNullOrWhiteSpace(name)) string reason = args.Parameters["reason"];
return RestMissingParam("ip", "name"); if (string.IsNullOrWhiteSpace(reason))
reason = "Banned";
try if (!DateTime.TryParse(args.Parameters["start"], out DateTime startDate))
startDate = DateTime.UtcNow;
if (!DateTime.TryParse(args.Parameters["end"], out DateTime endDate))
endDate = DateTime.MaxValue;
if (TShock.Bans.InsertBan(identifier, reason, args.TokenData.Username, startDate, endDate) != null)
{ {
TShock.Bans.AddBan(ip, name, "", "", args.Parameters["reason"], true, args.TokenData.Username); TSPlayer player = null;
if (identifier.StartsWith(Ban.Identifiers.IP))
{
player = TShock.Players.FirstOrDefault(p => p.IP == identifier.Substring(Ban.Identifiers.IP.Length));
}
else if (identifier.StartsWith(Ban.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)))
{
ply.Kick(reason, true);
}
}
else if (identifier.StartsWith(Ban.Identifiers.Account))
{
player = TShock.Players.FirstOrDefault(p => p.Account?.Name == identifier.Substring(Ban.Identifiers.Account.Length));
}
else if (identifier.StartsWith(Ban.Identifiers.UUID))
{
player = TShock.Players.FirstOrDefault(p => p.UUID == identifier.Substring(Ban.Identifiers.UUID.Length));
}
if (player != null)
{
player.Kick(reason, true);
}
return RestResponse("Ban added.");
} }
catch (Exception e)
{ return RestError("Failed to add ban.", status: "500");
return RestError(e.Message);
}
return RestResponse("Ban created successfully");
} }
[Description("Delete an existing ban entry.")] [Description("Delete an existing ban entry.")]
[Route("/v2/bans/destroy")] [Route("/v3/bans/destroy")]
[Permission(RestPermissions.restmanagebans)] [Permission(RestPermissions.restmanagebans)]
[Noun("ban", true, "The search criteria, either an IP address or a name.", typeof(String))] [Noun("identifier", true, "The identifier of the ban to delete.", typeof(String))]
[Noun("type", true, "The type of search criteria, 'ip' or 'name'. Also used as the method of removing from the database.", typeof(String))] [Noun("fullDelete", false, "Whether or not to completely remove the ban from the system.", typeof(bool))]
[Noun("caseinsensitive", false, "Name lookups should be case insensitive.", typeof(bool))]
[Token] [Token]
private object BanDestroyV2(RestRequestArgs args) private object BanDestroyV3(RestRequestArgs args)
{ {
var ret = BanFind(args.Parameters); string identifier = args.Parameters["identifier"];
if (ret is RestObject) if (string.IsNullOrWhiteSpace(identifier))
return ret; return RestMissingParam("identifier");
try bool.TryParse(args.Parameters["fullDelete"], out bool fullDelete);
{
Ban ban = (Ban)ret;
switch (args.Parameters["type"])
{
case "ip":
if (!TShock.Bans.RemoveBan(ban.IP, false, false, true))
return RestResponse("Failed to delete ban (already deleted?)");
break;
case "name":
if (!TShock.Bans.RemoveBan(ban.Name, true, GetBool(args.Parameters["caseinsensitive"], true)))
return RestResponse("Failed to delete ban (already deleted?)");
break;
default:
return RestError("Invalid Type: '" + args.Parameters["type"] + "'");
}
} if (TShock.Bans.RemoveBan(identifier, fullDelete))
catch (Exception e)
{ {
return RestError(e.Message); return RestResponse("Ban removed.");
} }
return RestResponse("Ban deleted successfully"); return RestError("Failed to remove ban.", status: "500");
} }
[Description("View the details of a specific ban.")] [Description("View the details of a specific ban.")]
[Route("/v2/bans/read")] [Route("/v3/bans/read")]
[Permission(RestPermissions.restviewbans)] [Permission(RestPermissions.restviewbans)]
[Noun("ban", true, "The search criteria, either an IP address or a name.", typeof(String))] [Noun("identifier", true, "The identifier to search for.", typeof(String))]
[Noun("type", true, "The type of search criteria, 'ip' or 'name'.", typeof(String))]
[Noun("caseinsensitive", false, "Name lookups should be case insensitive.", typeof(bool))]
[Token] [Token]
private object BanInfoV2(RestRequestArgs args) private object BanInfoV3(RestRequestArgs args)
{ {
var ret = BanFind(args.Parameters); string identifier = args.Parameters["identifier"];
if (ret is RestObject) if (string.IsNullOrWhiteSpace(identifier))
return ret; return RestMissingParam("identifier");
Ban ban = (Ban)ret; Ban ban = TShock.Bans.GetBanByIdentifier(identifier);
return new RestObject() {
{"name", null == ban.Name ? "" : ban.Name}, if (ban == null)
{"ip", null == ban.IP ? "" : ban.IP}, {
{"banning_user", null == ban.BanningUser ? "" : ban.BanningUser}, return RestResponse("No matching bans found.");
{"date", null == ban.BanDateTime ? "" : ban.BanDateTime.Value.ToString()}, }
{"reason", null == ban.Reason ? "" : ban.Reason},
return new RestObject
{
{"identifier", ban.Identifier },
{"reason", ban.Reason },
{"banning_user", ban.BanningUser },
{"fromDate", ban.BanDateTime.ToString("s") },
{"toDate", ban.ExpirationDateTime.ToString("s") },
}; };
} }
[Description("View all bans in the TShock database.")] [Description("View all bans in the TShock database.")]
[Route("/v2/bans/list")] [Route("/v3/bans/list")]
[Permission(RestPermissions.restviewbans)] [Permission(RestPermissions.restviewbans)]
[Token] [Token]
private object BanListV2(RestRequestArgs args) private object BanListV3(RestRequestArgs args)
{ {
IEnumerable<Ban> bans = TShock.Bans.GetAllBans();
var banList = new ArrayList(); var banList = new ArrayList();
foreach (var ban in TShock.Bans.GetBans()) foreach (var ban in bans)
{ {
banList.Add( banList.Add(
new Dictionary<string, string> new Dictionary<string, string>
{ {
{"name", null == ban.Name ? "" : ban.Name}, {"identifier", ban.Identifier },
{"ip", null == ban.IP ? "" : ban.IP}, {"reason", ban.Reason },
{"banning_user", null == ban.BanningUser ? "" : ban.BanningUser}, {"banning_user", ban.BanningUser },
{"date", null == ban.BanDateTime ? "" : ban.BanDateTime.Value.ToString()}, {"fromDate", ban.BanDateTime.ToString("s") },
{"reason", null == ban.Reason ? "" : ban.Reason}, {"toDate", ban.ExpirationDateTime.ToString("s") },
} }
); );
} }
return new RestObject() { { "bans", banList } }; return new RestObject
{
{ "bans", banList }
};
} }
#endregion #endregion
@ -987,26 +1016,6 @@ namespace TShockAPI
return RestResponse("Player " + player.Name + " was kicked"); return RestResponse("Player " + player.Name + " was kicked");
} }
[Description("Add a ban to the database.")]
[Route("/v2/players/ban")]
[Permission(RestPermissions.restban)]
[Permission(RestPermissions.restmanagebans)]
[Noun("player", true, "The player to kick.", typeof(String))]
[Noun("reason", false, "The reason the user was banned.", typeof(String))]
[Token]
private object PlayerBanV2(RestRequestArgs args)
{
var ret = PlayerFind(args.Parameters);
if (ret is RestObject)
return ret;
TSPlayer player = (TSPlayer)ret;
var reason = null == args.Parameters["reason"] ? "Banned via web" : args.Parameters["reason"];
TShock.Bans.AddBan(player.IP, player.Name, "", "", reason);
player.Kick(reason, true, false, null, true);
return RestResponse("Player " + player.Name + " was banned");
}
[Description("Kill a player.")] [Description("Kill a player.")]
[Route("/v2/players/kill")] [Route("/v2/players/kill")]
[Permission(RestPermissions.restkill)] [Permission(RestPermissions.restkill)]
@ -1039,7 +1048,7 @@ namespace TShockAPI
var groups = new ArrayList(); var groups = new ArrayList();
foreach (Group group in TShock.Groups) foreach (Group group in TShock.Groups)
{ {
groups.Add(new Dictionary<string, object> {{"name", group.Name}, {"parent", group.ParentName}, {"chatcolor", group.ChatColor}}); groups.Add(new Dictionary<string, object> { { "name", group.Name }, { "parent", group.ParentName }, { "chatcolor", group.ChatColor } });
} }
return new RestObject() { { "groups", groups } }; return new RestObject() { { "groups", groups } };
} }
@ -1200,7 +1209,7 @@ namespace TShockAPI
} }
} }
sb.AppendLine("Example Usage: {0}?{1}".SFormat(routeattr.Route, sb.AppendLine("Example Usage: {0}?{1}".SFormat(routeattr.Route,
string.Join("&", nouns.Select(n => String.Format("{0}={0}", ((Noun) n).Name))))); string.Join("&", nouns.Select(n => String.Format("{0}={0}", ((Noun)n).Name)))));
sb.AppendLine(); sb.AppendLine();
} }
} }
@ -1210,12 +1219,12 @@ namespace TShockAPI
private RestObject RestError(string message, string status = "400") private RestObject RestError(string message, string status = "400")
{ {
return new RestObject(status) {Error = message}; return new RestObject(status) { Error = message };
} }
private RestObject RestResponse(string message, string status = "200") private RestObject RestResponse(string message, string status = "200")
{ {
return new RestObject(status) {Response = message}; return new RestObject(status) { Response = message };
} }
private RestObject RestMissingParam(string var) private RestObject RestMissingParam(string var)
@ -1246,7 +1255,7 @@ namespace TShockAPI
return RestMissingParam("player"); return RestMissingParam("player");
var found = TSPlayer.FindByNameOrID(name); var found = TSPlayer.FindByNameOrID(name);
switch(found.Count) switch (found.Count)
{ {
case 1: case 1:
return found[0]; return found[0];
@ -1292,35 +1301,6 @@ namespace TShockAPI
return account; return account;
} }
private object BanFind(IParameterCollection parameters)
{
string name = parameters["ban"];
if (string.IsNullOrWhiteSpace(name))
return RestMissingParam("ban");
string type = parameters["type"];
if (string.IsNullOrWhiteSpace(type))
return RestMissingParam("type");
Ban ban;
switch (type)
{
case "ip":
ban = TShock.Bans.GetBanByIp(name);
break;
case "name":
ban = TShock.Bans.GetBanByName(name, GetBool(parameters["caseinsensitive"], true));
break;
default:
return RestError("Invalid Type: '" + type + "'");
}
if (null == ban)
return RestError("Ban " + type + " '" + name + "' doesn't exist");
return ban;
}
private object GroupFind(IParameterCollection parameters) private object GroupFind(IParameterCollection parameters)
{ {
var name = parameters["group"]; var name = parameters["group"];

View file

@ -1638,9 +1638,13 @@ namespace TShockAPI
return true; return true;
if (force || !HasPermission(Permissions.immunetoban)) if (force || !HasPermission(Permissions.immunetoban))
{ {
string ip = IP; TShock.Bans.InsertBan($"{DB.Ban.Identifiers.IP}{IP}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue);
string uuid = UUID; TShock.Bans.InsertBan($"{DB.Ban.Identifiers.IP}{UUID}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue);
TShock.Bans.AddBan(ip, Name, uuid, "", reason, false, adminUserName); if (Account != null)
{
TShock.Bans.InsertBan($"{DB.Ban.Identifiers.Account}{Account.Name}", reason, adminUserName, DateTime.UtcNow, DateTime.MaxValue);
}
Disconnect(string.Format("Banned: {0}", reason)); Disconnect(string.Format("Banned: {0}", reason));
string verb = force ? "force " : ""; string verb = force ? "force " : "";
if (string.IsNullOrWhiteSpace(adminUserName)) if (string.IsNullOrWhiteSpace(adminUserName))

View file

@ -478,26 +478,13 @@ namespace TShockAPI
args.Player.Account.KnownIps = JsonConvert.SerializeObject(KnownIps, Formatting.Indented); args.Player.Account.KnownIps = JsonConvert.SerializeObject(KnownIps, Formatting.Indented);
UserAccounts.UpdateLogin(args.Player.Account); UserAccounts.UpdateLogin(args.Player.Account);
Ban potentialBan = Bans.GetBanByAccountName(args.Player.Account.Name); //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));
if (potentialBan != null) //If they do and any are still valid, kick them
if (validBan != null)
{ {
// A user just signed in successfully despite being banned by account name. args.Player.Kick($"You are banned: {validBan.Reason}", true, true);
// We should fix the ban database so that all of their ban info is up to date.
Bans.AddBan(args.Player.IP, args.Player.Name, args.Player.UUID, args.Player.Account.Name,
potentialBan.Reason, false, potentialBan.BanningUser, potentialBan.Expiration);
// And then get rid of them.
if (potentialBan.Expiration == "")
{
args.Player.Kick(String.Format("Permanently banned by {0} for {1}", potentialBan.BanningUser
,potentialBan.Reason), true, true);
}
else
{
args.Player.Kick(String.Format("Still banned by {0} for {1}", potentialBan.BanningUser,
potentialBan.Reason), true, true);
}
} }
} }
@ -816,7 +803,7 @@ namespace TShockAPI
Console.WriteLine("Startup parameter overrode REST port."); Console.WriteLine("Startup parameter overrode REST port.");
} }
}) })
.AddFlags(playerSet, (p)=> .AddFlags(playerSet, (p) =>
{ {
int slots; int slots;
if (int.TryParse(p, out slots)) if (int.TryParse(p, out slots))
@ -1100,7 +1087,7 @@ namespace TShockAPI
if (args.Handled) if (args.Handled)
return; return;
if(!OnCreep(args.Grass)) if (!OnCreep(args.Grass))
{ {
args.Handled = true; args.Handled = true;
} }
@ -1209,65 +1196,45 @@ namespace TShockAPI
return; return;
} }
Ban ban = null; Ban ban = Bans.GetBansByIdentifiers($"name:{player.Name}", $"uuid:{player.UUID}", $"ip:{player.IP}").FirstOrDefault(b => Bans.IsValidBan(b));
if (Config.EnableBanOnUsernames)
{
var newban = Bans.GetBanByName(player.Name);
if (null != newban)
ban = newban;
}
if (Config.EnableIPBans && null == ban)
{
ban = Bans.GetBanByIp(player.IP);
}
if (Config.EnableUUIDBans && null == ban && !String.IsNullOrWhiteSpace(player.UUID))
{
ban = Bans.GetBanByUUID(player.UUID);
}
if (ban != null) if (ban != null)
{ {
if (!Bans.RemoveBanIfExpired(ban)) if (ban.ExpirationDateTime == DateTime.MaxValue)
{ {
DateTime exp; player.Disconnect("You are banned: " + ban.Reason);
if (!DateTime.TryParse(ban.Expiration, out exp)) }
else
{
TimeSpan ts = ban.ExpirationDateTime - DateTime.UtcNow;
int months = ts.Days / 30;
if (months > 0)
{ {
player.Disconnect("Permanently banned for: " + ban.Reason); 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 else
{ {
TimeSpan ts = exp - DateTime.UtcNow; player.Disconnect(String.Format("You are banned for {0} second{1}: {2}",
int months = ts.Days / 30; ts.Seconds, ts.Seconds == 1 ? "" : "s", ban.Reason));
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));
}
} }
args.Handled = true;
} }
args.Handled = true;
} }
} }
@ -1647,7 +1614,8 @@ namespace TShockAPI
player.RecentlyCreatedProjectiles.RemoveAll(p => p.Index == e.number && p.Killed); player.RecentlyCreatedProjectiles.RemoveAll(p => p.Index == e.number && p.Killed);
} }
if (!player.RecentlyCreatedProjectiles.Any(p => p.Index == e.number)) { if (!player.RecentlyCreatedProjectiles.Any(p => p.Index == e.number))
{
player.RecentlyCreatedProjectiles.Add(new GetDataHandlers.ProjectileStruct() player.RecentlyCreatedProjectiles.Add(new GetDataHandlers.ProjectileStruct()
{ {
Index = e.number, Index = e.number,

View file

@ -226,7 +226,7 @@
</PropertyGroup> </PropertyGroup>
<ProjectExtensions> <ProjectExtensions>
<VisualStudio> <VisualStudio>
<UserProperties BuildVersion_UpdateAssemblyVersion="True" BuildVersion_UpdateFileVersion="True" BuildVersion_BuildAction="Both" BuildVersion_BuildVersioningStyle="None.None.None.MonthAndDayStamp" BuildVersion_StartDate="2011/6/17" BuildVersion_IncrementBeforeBuild="False" /> <UserProperties BuildVersion_IncrementBeforeBuild="False" BuildVersion_StartDate="2011/6/17" BuildVersion_BuildVersioningStyle="None.None.None.MonthAndDayStamp" BuildVersion_BuildAction="Both" BuildVersion_UpdateFileVersion="True" BuildVersion_UpdateAssemblyVersion="True" />
</VisualStudio> </VisualStudio>
</ProjectExtensions> </ProjectExtensions>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View file

@ -531,6 +531,11 @@ namespace TShockAPI
{ {
seconds = 0; seconds = 0;
if (string.IsNullOrWhiteSpace(str))
{
return false;
}
var sb = new StringBuilder(3); var sb = new StringBuilder(3);
for (int i = 0; i < str.Length; i++) for (int i = 0; i < str.Length; i++)
{ {