mirror of
https://github.com/AuthMe/AuthMeReloaded.git
synced 2024-12-28 19:47:35 +01:00
#1034 Add debug sections for viewing DB data and Limbo data
This commit is contained in:
parent
4bb10c5d6d
commit
7eadb7f7f9
@ -20,7 +20,7 @@ public class DebugCommand implements ExecutableCommand {
|
||||
private Factory<DebugSection> debugSectionFactory;
|
||||
|
||||
private Set<Class<? extends DebugSection>> sectionClasses =
|
||||
ImmutableSet.of(PermissionGroups.class, TestEmailSender.class);
|
||||
ImmutableSet.of(PermissionGroups.class, TestEmailSender.class, PlayerAuthViewer.class, LimboPlayerViewer.class);
|
||||
|
||||
private Map<String, DebugSection> sections;
|
||||
|
||||
|
@ -0,0 +1,55 @@
|
||||
package fr.xephi.authme.command.executable.authme.debug;
|
||||
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
/**
|
||||
* Utilities used within the DebugSection implementations.
|
||||
*/
|
||||
final class DebugSectionUtils {
|
||||
|
||||
private DebugSectionUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given location in a human readable way. Null-safe.
|
||||
*
|
||||
* @param location the location to format
|
||||
* @return the formatted location
|
||||
*/
|
||||
static String formatLocation(Location location) {
|
||||
if (location == null) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
String worldName = location.getWorld() == null ? "null" : location.getWorld().getName();
|
||||
return formatLocation(location.getX(), location.getY(), location.getZ(), worldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given location in a human readable way.
|
||||
*
|
||||
* @param x the x coordinate
|
||||
* @param y the y coordinate
|
||||
* @param z the z coordinate
|
||||
* @param world the world name
|
||||
* @return the formatted location
|
||||
*/
|
||||
static String formatLocation(double x, double y, double z, String world) {
|
||||
return "(" + round(x) + ", " + round(y) + ", " + round(z) + ") in '" + world + "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the given number to two decimals.
|
||||
*
|
||||
* @param number the number to round
|
||||
* @return the rounded number
|
||||
*/
|
||||
private static String round(double number) {
|
||||
DecimalFormat df = new DecimalFormat("#.##");
|
||||
df.setRoundingMode(RoundingMode.HALF_UP);
|
||||
return df.format(number);
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package fr.xephi.authme.command.executable.authme.debug;
|
||||
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.data.limbo.LimboPlayer;
|
||||
import fr.xephi.authme.data.limbo.LimboService;
|
||||
import fr.xephi.authme.service.BukkitService;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
|
||||
|
||||
/**
|
||||
* Shows the data stored in LimboPlayers and the equivalent properties on online players.
|
||||
*/
|
||||
class LimboPlayerViewer implements DebugSection {
|
||||
|
||||
@Inject
|
||||
private LimboService limboService;
|
||||
|
||||
@Inject
|
||||
private BukkitService bukkitService;
|
||||
|
||||
private Field limboServiceEntries;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "limbo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "View LimboPlayers and player's \"limbo stats\"";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, List<String> arguments) {
|
||||
if (arguments.isEmpty()) {
|
||||
sender.sendMessage("/authme debug limbo <player>: show a player's limbo info");
|
||||
sender.sendMessage("Available limbo records: " + getLimboKeys());
|
||||
return;
|
||||
}
|
||||
|
||||
LimboPlayer limbo = limboService.getLimboPlayer(arguments.get(0));
|
||||
Player player = bukkitService.getPlayerExact(arguments.get(0));
|
||||
if (limbo == null && player == null) {
|
||||
sender.sendMessage("No limbo info and no player online with name '" + arguments.get(0) + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
sender.sendMessage(ChatColor.GOLD + "Showing limbo / player info for '" + arguments.get(0) + "'");
|
||||
new InfoDisplayer(sender, limbo, player)
|
||||
.sendEntry("Is op", LimboPlayer::isOperator, Player::isOp)
|
||||
.sendEntry("Walk speed", LimboPlayer::getWalkSpeed, Player::getWalkSpeed)
|
||||
.sendEntry("Can fly", LimboPlayer::isCanFly, Player::getAllowFlight)
|
||||
.sendEntry("Fly speed", LimboPlayer::getFlySpeed, Player::getFlySpeed)
|
||||
.sendEntry("Location", l -> formatLocation(l.getLocation()), p -> formatLocation(p.getLocation()))
|
||||
.sendEntry("Group", LimboPlayer::getGroup, p -> "");
|
||||
sender.sendMessage("Note: group is only shown for LimboPlayer");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the names of the LimboPlayers in the LimboService. As we don't want to expose this
|
||||
* information in non-debug settings, this is done over reflections. Since this is not a
|
||||
* crucial feature, we generously catch all Exceptions
|
||||
*
|
||||
* @return player names for which there is a LimboPlayer (or error message upon failure)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Set<String> getLimboKeys() {
|
||||
// Lazy initialization
|
||||
if (limboServiceEntries == null) {
|
||||
try {
|
||||
Field limboServiceEntries = LimboService.class.getDeclaredField("entries");
|
||||
limboServiceEntries.setAccessible(true);
|
||||
this.limboServiceEntries = limboServiceEntries;
|
||||
} catch (Exception e) {
|
||||
ConsoleLogger.logException("Could not retrieve LimboService entries field:", e);
|
||||
return Collections.singleton("Error retrieving LimboPlayer collection");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return (Set) ((Map) limboServiceEntries.get(limboService)).keySet();
|
||||
} catch (Exception e) {
|
||||
ConsoleLogger.logException("Could not retrieve LimboService values:", e);
|
||||
return Collections.singleton("Error retrieving LimboPlayer values");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the info for the given LimboPlayer and Player to the provided CommandSender.
|
||||
*/
|
||||
private static final class InfoDisplayer {
|
||||
private final CommandSender sender;
|
||||
private final Optional<LimboPlayer> limbo;
|
||||
private final Optional<Player> player;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param sender command sender to send the information to
|
||||
* @param limbo the limbo player to get data from
|
||||
* @param player the player to get data from
|
||||
*/
|
||||
InfoDisplayer(CommandSender sender, LimboPlayer limbo, Player player) {
|
||||
this.sender = sender;
|
||||
this.limbo = Optional.ofNullable(limbo);
|
||||
this.player = Optional.ofNullable(player);
|
||||
|
||||
if (limbo == null) {
|
||||
sender.sendMessage("Note: no Limbo information available");
|
||||
} else if (player == null) {
|
||||
sender.sendMessage("Note: player is not online");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a piece of information to the command sender.
|
||||
*
|
||||
* @param title the designation of the piece of information
|
||||
* @param limboGetter getter for data retrieval on the LimboPlayer
|
||||
* @param playerGetter getter for data retrieval on Player
|
||||
* @param <T> the data type
|
||||
* @return this instance (for chaining)
|
||||
*/
|
||||
<T> InfoDisplayer sendEntry(String title,
|
||||
Function<LimboPlayer, T> limboGetter,
|
||||
Function<Player, T> playerGetter) {
|
||||
sender.sendMessage(
|
||||
title + ": "
|
||||
+ limbo.map(limboGetter).map(String::valueOf).orElse("--")
|
||||
+ " / "
|
||||
+ player.map(playerGetter).map(String::valueOf).orElse("--"));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package fr.xephi.authme.command.executable.authme.debug;
|
||||
|
||||
import fr.xephi.authme.data.auth.PlayerAuth;
|
||||
import fr.xephi.authme.datasource.DataSource;
|
||||
import fr.xephi.authme.security.crypts.HashedPassword;
|
||||
import fr.xephi.authme.util.StringUtils;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
|
||||
|
||||
/**
|
||||
* Allows to view the data of a PlayerAuth in the database.
|
||||
*/
|
||||
class PlayerAuthViewer implements DebugSection {
|
||||
|
||||
@Inject
|
||||
private DataSource dataSource;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "db";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "View player's data in the database";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, List<String> arguments) {
|
||||
if (arguments.isEmpty()) {
|
||||
sender.sendMessage("Enter player name to view his data in the database.");
|
||||
sender.sendMessage("Example: /authme debug db Bobby");
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerAuth auth = dataSource.getAuth(arguments.get(0));
|
||||
if (auth == null) {
|
||||
sender.sendMessage("No record exists for '" + arguments.get(0) + "'");
|
||||
} else {
|
||||
displayAuthToSender(auth, sender);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the PlayerAuth information to the given sender.
|
||||
*
|
||||
* @param auth the PlayerAuth to display
|
||||
* @param sender the sender to send the messages to
|
||||
*/
|
||||
private void displayAuthToSender(PlayerAuth auth, CommandSender sender) {
|
||||
sender.sendMessage(ChatColor.GOLD + "[AuthMe] Player " + auth.getNickname() + " / " + auth.getRealName());
|
||||
sender.sendMessage("Email: " + auth.getEmail() + ". IP: " + auth.getIp() + ". Group: " + auth.getGroupId());
|
||||
sender.sendMessage("Quit location: "
|
||||
+ formatLocation(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld()));
|
||||
sender.sendMessage("Last login: " + formatLastLogin(auth));
|
||||
|
||||
HashedPassword hashedPass = auth.getPassword();
|
||||
sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6)
|
||||
+ "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail-safe substring method. Guarantees not to show the entire String.
|
||||
*
|
||||
* @param str the string to transform
|
||||
* @param length number of characters to show from the start of the String
|
||||
* @return the first <code>length</code> characters of the string, or half of the string if it is shorter,
|
||||
* or empty string if the string is null or empty
|
||||
*/
|
||||
private static String safeSubstring(String str, int length) {
|
||||
if (StringUtils.isEmpty(str)) {
|
||||
return "";
|
||||
} else if (str.length() < length) {
|
||||
return str.substring(0, str.length() / 2) + "...";
|
||||
} else {
|
||||
return str.substring(0, length) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the last login date from the given PlayerAuth.
|
||||
*
|
||||
* @param auth the auth object
|
||||
* @return the last login as human readable date
|
||||
*/
|
||||
private static String formatLastLogin(PlayerAuth auth) {
|
||||
long lastLogin = auth.getLastLogin();
|
||||
if (lastLogin == 0) {
|
||||
return "Never (0)";
|
||||
} else {
|
||||
LocalDateTime date = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastLogin), ZoneId.systemDefault());
|
||||
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(date);
|
||||
}
|
||||
}
|
||||
}
|
@ -41,8 +41,8 @@ class TestEmailSender implements DebugSection {
|
||||
@Override
|
||||
public void execute(CommandSender sender, List<String> arguments) {
|
||||
if (!sendMailSSL.hasAllInformation()) {
|
||||
sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml " +
|
||||
"for sending emails. Please check your config.yml");
|
||||
sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml "
|
||||
+ "for sending emails. Please check your config.yml");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -69,7 +69,8 @@ class TestEmailSender implements DebugSection {
|
||||
}
|
||||
String email = auth.getEmail();
|
||||
if (email == null || "your@email.com".equals(email)) {
|
||||
sender.sendMessage(ChatColor.RED + "No email set for your account! Please use /authme debug mail <email>");
|
||||
sender.sendMessage(ChatColor.RED + "No email set for your account!"
|
||||
+ " Please use /authme debug mail <email>");
|
||||
return null;
|
||||
}
|
||||
return email;
|
||||
|
@ -0,0 +1,46 @@
|
||||
package fr.xephi.authme.command.executable.authme.debug;
|
||||
|
||||
import fr.xephi.authme.TestHelper;
|
||||
import org.bukkit.Location;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link DebugSectionUtils}.
|
||||
*/
|
||||
public class DebugSectionUtilsTest {
|
||||
|
||||
@Test
|
||||
public void shouldFormatLocation() {
|
||||
// given / when
|
||||
String result = DebugSectionUtils.formatLocation(0.0, 10.248592, -18934.2349023, "Main");
|
||||
|
||||
// then
|
||||
assertThat(result, equalTo("(0, 10.25, -18934.23) in 'Main'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleNullWorld() {
|
||||
// given
|
||||
Location location = new Location(null, 3.7777, 2.14156, 1);
|
||||
|
||||
// when
|
||||
String result = DebugSectionUtils.formatLocation(location);
|
||||
|
||||
// then
|
||||
assertThat(result, equalTo("(3.78, 2.14, 1) in 'null'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleNullLocation() {
|
||||
// given / when / then
|
||||
assertThat(DebugSectionUtils.formatLocation(null), equalTo("null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHaveHiddenConstructor() {
|
||||
TestHelper.validateHasOnlyPrivateEmptyConstructor(DebugSectionUtils.class);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user