Merge pull request #2170 from Pryaxis/new-bans

New bans + some other stuff
This commit is contained in:
Chris 2020-12-03 14:14:55 +10:30 committed by GitHub
commit 3b1502c28f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1250 additions and 904 deletions

View file

@ -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`. Old bans will be converted automatically 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. New identifiers can be added to the `ban help identifiers` output by registering them in `TShockAPI.DB.Identifier.Register(string, string)`
* 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)

View file

@ -1267,328 +1267,369 @@ namespace TShockAPI
private static void Ban(CommandArgs args)
{
//Ban syntax:
// ban add <target> [reason] [duration] [flags (default: -a -u -ip)]
// Duration is in the format 0d0h0m0s. Any part can be ignored. E.g., 1s is a valid ban time, as is 1d1s, etc. If no duration is specified, ban is permanent
// 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 <ban ID>
// Target is expected to be a ban Unique ID
// ban list [page]
// Displays a paginated list of bans
// ban details <ban ID>
// Target is expected to be a ban Unique ID
//ban help [command]
// Provides extended help on specific ban commands
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 {"add".Color(Utils.RedHighlight)} <Target> [Flags]", Color.White);
args.Player.SendMessage($"ban {"del".Color(Utils.RedHighlight)} <Ban ID>", Color.White);
args.Player.SendMessage($"ban {"list".Color(Utils.RedHighlight)}", Color.White);
args.Player.SendMessage($"ban {"details".Color(Utils.RedHighlight)} <Ban ID>", Color.White);
args.Player.SendMessage($"Quick usage: {"ban add".Color(Utils.BoldHighlight)} {args.Player.Name.Color(Utils.RedHighlight)} \"Griefing\"", Color.White);
args.Player.SendMessage($"For more info, use {"ban help".Color(Utils.BoldHighlight)} {"command".Color(Utils.RedHighlight)}", 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($"{"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($" If no duration is provided, the ban will be permanent.", 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($"{"ban del".Color(Utils.BoldHighlight)} <{"Ticket Number".Color(Utils.RedHighlight)}>", Color.White);
args.Player.SendMessage($"- {"Ticket Number".Color(Utils.RedHighlight)}s are provided when you add a ban, and can also be viewed 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($"{"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: {"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($"{"ban details".Color(Utils.BoldHighlight)} <{"Ticket Number".Color(Utils.RedHighlight)}>", Color.White);
args.Player.SendMessage($"- {"Ticket Number".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;
case "identifiers":
if (!PaginationTools.TryParsePageNumber(args.Parameters, 2, args.Player, out int pageNumber))
{
args.Player.SendMessage($"Invalid page number. Page number should be numeric.", Color.White);
return;
}
var idents = from ident in Identifier.Available
select $"{ident.Color(Utils.RedHighlight)} - {ident.Description}";
args.Player.SendMessage("", Color.White);
PaginationTools.SendPage(args.Player, pageNumber, idents.ToList(),
new PaginationTools.Settings
{
HeaderFormat = "Available identifiers ({0}/{1}):",
FooterFormat = "Type {0}ban help identifiers {{0}} for more.".SFormat(Specifier),
NothingToDisplayString = "There are currently no available identifiers.",
HeaderTextColor = Color.White,
LineTextColor = Color.White
});
break;
default:
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)} - Ticket Number: {ban.TicketNumber.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($"{"Ban 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($"{"Ban expires:".Color(Utils.BoldHighlight)} {ban.ExpirationDateTime.ToString("yyyy/MM/dd").Color(Utils.RedHighlight)} ({remaining})", Color.White);
}
}
AddBanResult DoBan(string ident, string reason, DateTime expiration)
{
AddBanResult banResult = TShock.Bans.InsertBan(ident, reason, args.Player.Account.Name, DateTime.UtcNow, expiration);
if (banResult.Ban != null)
{
args.Player.SendSuccessMessage($"Ban added. Ticket Number {banResult.Ban.TicketNumber.Color(Utils.GreenHighlight)} was created for identifier {ident.Color(Utils.WhiteHighlight)}.");
}
else
{
args.Player.SendWarningMessage($"Failed to add ban for identifier: {ident.Color(Utils.WhiteHighlight)}");
args.Player.SendWarningMessage($"Reason: {banResult.Message}");
}
return banResult;
}
void AddBan()
{
if (!args.Parameters.TryGetValue(1, out string target))
{
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;
}
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");
List<string> flags = new List<string>() { "-e", "-a", "-u", "-n", "-ip" };
string reason = "Banned.";
string duration = null;
DateTime expiration = DateTime.MaxValue;
//This is hacky. We want flag values to be independent of order so we must force the consecutive ordering of the 'reason' and 'duration' parameters,
//while still allowing them to be placed arbitrarily in the parameter list.
//As an example, the following parameter lists (and more) should all be acceptable:
//-u "reason" -a duration -ip
//"reason" duration -u -a -ip
//-u -a -ip "reason" duration
//-u -a -ip
for (int i = 2; i < args.Parameters.Count; i++)
{
var param = args.Parameters[i];
if (!flags.Contains(param))
{
reason = param;
break;
}
}
for (int i = 3; i < args.Parameters.Count; i++)
{
var param = args.Parameters[i];
if (!flags.Contains(param))
{
duration = param;
break;
}
}
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;
}
reason = reason ?? "Banned";
if (exactTarget)
{
DoBan(target, reason, expiration);
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];
AddBanResult banResult = null;
if (banAccount)
{
if (player.Account != null)
{
banResult = DoBan($"{Identifier.Account}{player.Account.Name}", reason, expiration);
}
}
if (banUuid)
{
banResult = DoBan($"{Identifier.UUID}{player.UUID}", reason, expiration);
}
if (banName)
{
banResult = DoBan($"{Identifier.Name}{player.Name}", reason, expiration);
}
if (banIp)
{
banResult = DoBan($"{Identifier.IP}{player.IP}", reason, expiration);
}
if (banResult?.Ban != null)
{
player.Disconnect($"You have been banned: {banResult.Ban.Reason}.");
}
}
void DelBan()
{
if (!args.Parameters.TryGetValue(1, out string target))
{
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 (!int.TryParse(target, out int banId))
{
args.Player.SendMessage($"Invalid Ticket Number. 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))
{
TShock.Log.ConsoleInfo($"Ban {banId} has been revoked by {args.Player.Account.Name}.");
args.Player.SendSuccessMessage($"Ban {banId.Color(Utils.GreenHighlight)} has now been marked as expired.");
}
else
{
args.Player.SendErrorMessage("Failed to remove ban.");
}
}
void ListBans()
{
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}";
}
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out int pageNumber))
{
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))}";
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 active bans."
});
}
void BanDetails()
{
if (!args.Parameters.TryGetValue(1, out string target))
{
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;
}
if (!int.TryParse(target, out int banId))
{
args.Player.SendMessage($"Invalid Ticket Number. 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)
{
args.Player.SendErrorMessage("No bans found matching the provided ticket number");
return;
}
DisplayBanDetails(ban);
}
string subcmd = args.Parameters.Count == 0 ? "help" : args.Parameters[0].ToLower();
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":
#region Help
{
int pageNumber;
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out pageNumber))
return;
Help();
break;
var lines = new List<string>
{
"add <target> <time> [reason] - Bans a player or user account if the player is not online.",
"del <player> - Unbans a player.",
"delip <ip> - Unbans an IP.",
"list [page] - Lists all player bans.",
"listip [page] - Lists all IP bans."
};
case "add":
AddBan();
break;
case "del":
DelBan();
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":
#region List bans
{
int pageNumber;
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out pageNumber))
{
return;
}
ListBans();
break;
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:
args.Player.SendErrorMessage("Invalid subcommand! Type {0}ban help for more information.", Specifier);
return;
break;
}
}
@ -1844,7 +1885,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;

File diff suppressed because it is too large Load diff

View file

@ -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,39 @@ namespace TShockAPI.DB
}
}
/// <summary>
/// Executes a query on a database, returning the first column of the first row of the result set.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="olddb">Database to query</param>
/// <param name="query">Query string with parameters as @0, @1, etc.</param>
/// <param name="args">Parameters to be put in the query</param>
/// <returns></returns>
public static T QueryScalar<T>(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 (output.GetType() != typeof(T))
{
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<string, object> values)
{
var db = olddb.CloneEx();
@ -210,12 +242,24 @@ namespace TShockAPI.DB
public static T Get<T>(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);
}
}

View file

@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
namespace TShockAPI
{
@ -31,5 +32,25 @@ namespace TShockAPI
foreach (T item in source)
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

@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Microsoft.Xna.Framework;
using System;
namespace TShockAPI
@ -27,5 +28,16 @@ namespace TShockAPI
{
return String.Format(str, args);
}
/// <summary>
/// Wraps the string representation of an object with a Terraria color code for the given color
/// </summary>
/// <param name="obj"></param>
/// <param name="color"></param>
/// <returns></returns>
public static string Color(this object obj, string color)
{
return $"[c/{color}:{obj}]";
}
}
}
}

View file

@ -52,6 +52,7 @@ namespace TShockAPI
[Description("Prevents you from being kicked.")]
public static readonly string immunetokick = "tshock.admin.nokick";
[Obsolete("Ban immunity is no longer available.")]
[Description("Prevents you from being banned.")]
public static readonly string immunetoban = "tshock.admin.noban";

View file

@ -155,7 +155,7 @@ namespace TShockAPI
/// <summary>
/// Creates a new instance of <see cref="Token"/>
/// </summary>
public Token() : base("token", true, "The REST authentication token.", typeof(String)){}
public Token() : base("token", true, "The REST authentication token.", typeof(String)) { }
}
/// <summary>
@ -216,9 +216,13 @@ namespace TShockAPI
Rest.RegisterRedirect("/users/update", "/v2/users/update");
//ban commands
Rest.RegisterRedirect("/bans/list", "/v2/bans/list");
Rest.RegisterRedirect("/bans/read", "/v2/bans/read");
Rest.RegisterRedirect("/bans/destroy", "/v2/bans/destroy");
Rest.RegisterRedirect("/bans/create", "/v3/bans/create");
Rest.RegisterRedirect("/bans/list", "/v3/bans/list");
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
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 });
// Ban Commands
Rest.Register(new SecureRestCommand("/bans/create", BanCreate, RestPermissions.restmanagebans));
Rest.Register(new SecureRestCommand("/v2/bans/list", BanListV2, RestPermissions.restviewbans));
Rest.Register(new SecureRestCommand("/v2/bans/read", BanInfoV2, RestPermissions.restviewbans));
Rest.Register(new SecureRestCommand("/v2/bans/destroy", BanDestroyV2, RestPermissions.restmanagebans));
Rest.Register(new SecureRestCommand("/v3/bans/create", BanCreateV3, RestPermissions.restban, RestPermissions.restmanagebans));
Rest.Register(new SecureRestCommand("/v3/bans/list", BanListV3, RestPermissions.restviewbans));
Rest.Register(new SecureRestCommand("/v3/bans/read", BanInfoV3, RestPermissions.restviewbans));
Rest.Register(new SecureRestCommand("/v3/bans/destroy", BanDestroyV3, RestPermissions.restmanagebans));
// World Commands
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("/v4/players/read", PlayerReadV4, RestPermissions.restuserinfo));
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/mute", PlayerMute, RestPermissions.restmute));
Rest.Register(new SecureRestCommand("/v2/players/unmute", PlayerUnMute, RestPermissions.restmute));
@ -420,7 +423,7 @@ namespace TShockAPI
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("DisableBuild", TShock.Config.DisableBuild);
rules.Add("DisableClownBombs", TShock.Config.DisableClownBombs);
@ -492,8 +495,8 @@ namespace TShockAPI
return RestMissingParam("user");
var group = args.Parameters["group"];
if (string.IsNullOrWhiteSpace(group))
group = TShock.Config.DefaultRegistrationGroupName;
if (string.IsNullOrWhiteSpace(group))
group = TShock.Config.DefaultRegistrationGroupName;
var password = args.Parameters["password"];
if (string.IsNullOrWhiteSpace(password))
@ -609,115 +612,154 @@ namespace TShockAPI
#region Rest Ban Methods
[Description("Create a new ban entry.")]
[Route("/bans/create")]
[Route("/v3/bans/create")]
[Permission(RestPermissions.restmanagebans)]
[Noun("ip", false, "The IP to ban, at least this or name must be specified.", typeof(String))]
[Noun("name", false, "The name to ban, at least this or ip must be specified.", typeof(String))]
[Noun("identifier", true, "The identifier to 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]
private object BanCreate(RestRequestArgs args)
private object BanCreateV3(RestRequestArgs args)
{
var ip = args.Parameters["ip"];
var name = args.Parameters["name"];
string identifier = args.Parameters["identifier"];
if (string.IsNullOrWhiteSpace(identifier))
return RestMissingParam("identifier");
if (string.IsNullOrWhiteSpace(ip) && string.IsNullOrWhiteSpace(name))
return RestMissingParam("ip", "name");
string reason = args.Parameters["reason"];
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;
AddBanResult banResult = TShock.Bans.InsertBan(identifier, reason, args.TokenData.Username, startDate, endDate);
if (banResult.Ban != null)
{
TShock.Bans.AddBan(ip, name, "", "", args.Parameters["reason"], true, args.TokenData.Username);
TSPlayer player = null;
if (identifier.StartsWith(Identifier.IP.Prefix))
{
player = TShock.Players.FirstOrDefault(p => p.IP == identifier.Substring(Identifier.IP.Prefix.Length));
}
else if (identifier.StartsWith(Identifier.Name.Prefix))
{
//Character names may not necessarily be unique, so kick all matches
foreach (var ply in TShock.Players.Where(p => p.Name == identifier.Substring(Identifier.Name.Prefix.Length)))
{
ply.Kick(reason, true);
}
}
else if (identifier.StartsWith(Identifier.Account.Prefix))
{
player = TShock.Players.FirstOrDefault(p => p.Account?.Name == identifier.Substring(Identifier.Account.Prefix.Length));
}
else if (identifier.StartsWith(Identifier.UUID.Prefix))
{
player = TShock.Players.FirstOrDefault(p => p.UUID == identifier.Substring(Identifier.UUID.Prefix.Length));
}
if (player != null)
{
player.Kick(reason, true);
}
return RestResponse($"Ban added. Ticket number: {banResult.Ban.TicketNumber}");
}
catch (Exception e)
{
return RestError(e.Message);
}
return RestResponse("Ban created successfully");
return RestError($"Failed to add ban. {banResult.Message}", status: "500");
}
[Description("Delete an existing ban entry.")]
[Route("/v2/bans/destroy")]
[Route("/v3/bans/destroy")]
[Permission(RestPermissions.restmanagebans)]
[Noun("ban", true, "The search criteria, either an IP address or a name.", 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("caseinsensitive", false, "Name lookups should be case insensitive.", typeof(bool))]
[Noun("ticketNumber", true, "The ticket number 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 BanDestroyV2(RestRequestArgs args)
private object BanDestroyV3(RestRequestArgs args)
{
var ret = BanFind(args.Parameters);
if (ret is RestObject)
return ret;
string id = args.Parameters["ticketNumber"];
if (string.IsNullOrWhiteSpace(id))
return RestMissingParam("ticketNumber");
try
if (!int.TryParse(id, out int ticketNumber))
{
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"] + "'");
}
}
catch (Exception e)
{
return RestError(e.Message);
return RestInvalidParam("ticketNumber");
}
return RestResponse("Ban deleted successfully");
bool.TryParse(args.Parameters["fullDelete"], out bool fullDelete);
if (TShock.Bans.RemoveBan(ticketNumber, fullDelete))
{
return RestResponse("Ban removed.");
}
return RestError("Failed to remove ban.", status: "500");
}
[Description("View the details of a specific ban.")]
[Route("/v2/bans/read")]
[Route("/v3/bans/read")]
[Permission(RestPermissions.restviewbans)]
[Noun("ban", true, "The search criteria, either an IP address or a name.", 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))]
[Noun("ticketNumber", true, "The ticket number to search for.", typeof(String))]
[Token]
private object BanInfoV2(RestRequestArgs args)
private object BanInfoV3(RestRequestArgs args)
{
var ret = BanFind(args.Parameters);
if (ret is RestObject)
return ret;
string id = args.Parameters["ticketNumber"];
if (string.IsNullOrWhiteSpace(id))
return RestMissingParam("ticketNumber");
Ban ban = (Ban)ret;
return new RestObject() {
{"name", null == ban.Name ? "" : ban.Name},
{"ip", null == ban.IP ? "" : ban.IP},
{"banning_user", null == ban.BanningUser ? "" : ban.BanningUser},
{"date", null == ban.BanDateTime ? "" : ban.BanDateTime.Value.ToString()},
{"reason", null == ban.Reason ? "" : ban.Reason},
if (!int.TryParse(id, out int ticketNumber))
{
return RestInvalidParam("ticketNumber");
}
Ban ban = TShock.Bans.GetBanById(ticketNumber);
if (ban == null)
{
return RestResponse("No matching bans found.");
}
return new RestObject
{
{ "ticket_number", ban.TicketNumber },
{ "identifier", ban.Identifier },
{ "reason", ban.Reason },
{ "banning_user", ban.BanningUser },
{ "start_date_ticks", ban.BanDateTime.Ticks },
{ "end_date_ticks", ban.ExpirationDateTime.Ticks },
};
}
[Description("View all bans in the TShock database.")]
[Route("/v2/bans/list")]
[Route("/v3/bans/list")]
[Permission(RestPermissions.restviewbans)]
[Token]
private object BanListV2(RestRequestArgs args)
private object BanListV3(RestRequestArgs args)
{
IEnumerable<Ban> bans = TShock.Bans.Bans.Select(kvp => kvp.Value);
var banList = new ArrayList();
foreach (var ban in TShock.Bans.GetBans())
foreach (var ban in bans)
{
banList.Add(
new Dictionary<string, string>
new Dictionary<string, object>
{
{"name", null == ban.Name ? "" : ban.Name},
{"ip", null == ban.IP ? "" : ban.IP},
{"banning_user", null == ban.BanningUser ? "" : ban.BanningUser},
{"date", null == ban.BanDateTime ? "" : ban.BanDateTime.Value.ToString()},
{"reason", null == ban.Reason ? "" : ban.Reason},
{ "ticket_number", ban.TicketNumber },
{ "identifier", ban.Identifier },
{ "reason", ban.Reason },
{ "banning_user", ban.BanningUser },
{ "start_date_ticks", ban.BanDateTime.Ticks },
{ "end_date_ticks", ban.ExpirationDateTime.Ticks },
}
);
}
return new RestObject() { { "bans", banList } };
return new RestObject
{
{ "bans", banList }
};
}
#endregion
@ -987,26 +1029,6 @@ namespace TShockAPI
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.")]
[Route("/v2/players/kill")]
[Permission(RestPermissions.restkill)]
@ -1039,7 +1061,7 @@ namespace TShockAPI
var groups = new ArrayList();
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 } };
}
@ -1200,7 +1222,7 @@ namespace TShockAPI
}
}
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();
}
}
@ -1210,12 +1232,12 @@ namespace TShockAPI
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")
{
return new RestObject(status) {Response = message};
return new RestObject(status) { Response = message };
}
private RestObject RestMissingParam(string var)
@ -1246,7 +1268,7 @@ namespace TShockAPI
return RestMissingParam("player");
var found = TSPlayer.FindByNameOrID(name);
switch(found.Count)
switch (found.Count)
{
case 1:
return found[0];
@ -1292,35 +1314,6 @@ namespace TShockAPI
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)
{
var name = parameters["group"];

View file

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

View file

@ -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<Color, ConsoleColor> _consoleColorMap = new Dictionary<Color, ConsoleColor>
{
{ 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);
}
}
}
}

View file

@ -478,27 +478,7 @@ namespace TShockAPI
args.Player.Account.KnownIps = JsonConvert.SerializeObject(KnownIps, Formatting.Indented);
UserAccounts.UpdateLogin(args.Player.Account);
Ban potentialBan = Bans.GetBanByAccountName(args.Player.Account.Name);
if (potentialBan != null)
{
// A user just signed in successfully despite being banned by account name.
// 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);
}
}
Bans.CheckBan(args.Player);
}
/// <summary>OnAccountDelete - Internal hook fired on account delete.</summary>
@ -816,7 +796,7 @@ namespace TShockAPI
Console.WriteLine("Startup parameter overrode REST port.");
}
})
.AddFlags(playerSet, (p)=>
.AddFlags(playerSet, (p) =>
{
int slots;
if (int.TryParse(p, out slots))
@ -1100,7 +1080,7 @@ namespace TShockAPI
if (args.Handled)
return;
if(!OnCreep(args.Grass))
if (!OnCreep(args.Grass))
{
args.Handled = true;
}
@ -1209,66 +1189,7 @@ namespace TShockAPI
return;
}
Ban ban = null;
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 (!Bans.RemoveBanIfExpired(ban))
{
DateTime exp;
if (!DateTime.TryParse(ban.Expiration, out exp))
{
player.Disconnect("Permanently banned for: " + ban.Reason);
}
else
{
TimeSpan ts = exp - 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));
}
}
args.Handled = true;
}
}
Bans.CheckBan(player);
}
/// <summary>OnLeave - Called when a player leaves the server.</summary>
@ -1647,7 +1568,8 @@ namespace TShockAPI
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()
{
Index = e.number,

View file

@ -27,7 +27,6 @@ using System.Text.RegularExpressions;
using Terraria;
using Terraria.ID;
using Terraria.Utilities;
using TShockAPI.DB;
using Microsoft.Xna.Framework;
using Terraria.Localization;
using TShockAPI.Localization;
@ -39,6 +38,31 @@ namespace TShockAPI
/// </summary>
public class Utils
{
/// <summary>
/// Hex code for a blue pastel color
/// </summary>
public const string BoldHighlight = "AAAAFF";
/// <summary>
/// Hex code for a red pastel color
/// </summary>
public const string RedHighlight = "FFAAAA";
/// <summary>
/// Hex code for a green pastel color
/// </summary>
public const string GreenHighlight = "AAFFAA";
/// <summary>
/// Hex code for a pink pastel color
/// </summary>
public const string PinkHighlight = "FFAAFF";
/// <summary>
/// Hex code for a yellow pastel color
/// </summary>
public const string YellowHighlight = "FFFAAA";
/// <summary>
/// Hex code for a white highlight
/// </summary>
public const string WhiteHighlight = "FFFFFF";
/// <summary>
/// The lowest id for a prefix.
/// </summary>
@ -465,7 +489,14 @@ namespace TShockAPI
if (save)
SaveManager.Instance.SaveWorld();
TSPlayer.All.Kick(reason, true, true, null, true);
foreach (var player in TShock.Players.Where(p => p != null))
{
if (player.IsLoggedIn)
{
player.SaveServerCharacter();
}
player.Disconnect(reason);
}
// Broadcast so console can see we are shutting down as well
TShock.Utils.Broadcast(reason, Color.Red);
@ -531,6 +562,11 @@ namespace TShockAPI
{
seconds = 0;
if (string.IsNullOrWhiteSpace(str))
{
return false;
}
var sb = new StringBuilder(3);
for (int i = 0; i < str.Length; i++)
{