#1254 Create command to see recently logged in players

- Create datasource method to fetch most recent players by last login date
- Add command to view last logged in players
This commit is contained in:
ljacqu 2017-11-28 21:07:10 +01:00
parent 7932c1bf90
commit 50dbbb8d87
11 changed files with 205 additions and 1 deletions

View File

@ -17,6 +17,7 @@ import fr.xephi.authme.command.executable.authme.PurgeBannedPlayersCommand;
import fr.xephi.authme.command.executable.authme.PurgeCommand; import fr.xephi.authme.command.executable.authme.PurgeCommand;
import fr.xephi.authme.command.executable.authme.PurgeLastPositionCommand; import fr.xephi.authme.command.executable.authme.PurgeLastPositionCommand;
import fr.xephi.authme.command.executable.authme.PurgePlayerCommand; import fr.xephi.authme.command.executable.authme.PurgePlayerCommand;
import fr.xephi.authme.command.executable.authme.RecentPlayersCommand;
import fr.xephi.authme.command.executable.authme.RegisterAdminCommand; import fr.xephi.authme.command.executable.authme.RegisterAdminCommand;
import fr.xephi.authme.command.executable.authme.ReloadCommand; import fr.xephi.authme.command.executable.authme.ReloadCommand;
import fr.xephi.authme.command.executable.authme.SetEmailCommand; import fr.xephi.authme.command.executable.authme.SetEmailCommand;
@ -433,6 +434,15 @@ public class CommandInitializer {
.executableCommand(MessagesCommand.class) .executableCommand(MessagesCommand.class)
.register(); .register();
CommandDescription.builder()
.parent(authmeBase)
.labels("recent")
.description("See players who have recently logged in")
.detailedDescription("Shows the last players that have logged in.")
.permission(AdminPermission.SEE_RECENT_PLAYERS)
.executableCommand(RecentPlayersCommand.class)
.register();
CommandDescription.builder() CommandDescription.builder()
.parent(authmeBase) .parent(authmeBase)
.labels("debug", "dbg") .labels("debug", "dbg")

View File

@ -0,0 +1,50 @@
package fr.xephi.authme.command.executable.authme;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static java.time.Instant.ofEpochMilli;
/**
* Command showing the most recent logged in players.
*/
public class RecentPlayersCommand implements ExecutableCommand {
/** DateTime formatter, producing Strings such as "10:42 AM, 11 Jul". */
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("hh:mm a, dd MMM");
@Inject
private DataSource dataSource;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
List<PlayerAuth> recentPlayers = dataSource.getRecentlyLoggedInPlayers();
sender.sendMessage(ChatColor.BLUE + "[AuthMe] Recently logged in players");
for (PlayerAuth auth : recentPlayers) {
sender.sendMessage(formatPlayerMessage(auth));
}
}
@VisibleForTesting
ZoneId getZoneId() {
return ZoneId.systemDefault();
}
private String formatPlayerMessage(PlayerAuth auth) {
LocalDateTime lastLogin = LocalDateTime.ofInstant(ofEpochMilli(auth.getLastLogin()), getZoneId());
String lastLoginText = DATE_FORMAT.format(lastLogin);
return "- " + auth.getRealName() + " (" + lastLoginText + " with IP " + auth.getLastIp() + ")";
}
}

View File

@ -263,6 +263,11 @@ public class CacheDataSource implements DataSource {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public List<PlayerAuth> getRecentlyLoggedInPlayers() {
return source.getRecentlyLoggedInPlayers();
}
@Override @Override
public void invalidateCache(String playerName) { public void invalidateCache(String playerName) {
cachedAuths.invalidate(playerName); cachedAuths.invalidate(playerName);

View File

@ -225,6 +225,13 @@ public interface DataSource extends Reloadable {
*/ */
List<PlayerAuth> getAllAuths(); List<PlayerAuth> getAllAuths();
/**
* Returns the last ten players who have recently logged in (first ten players with highest last login date).
*
* @return the 10 last players who last logged in
*/
List<PlayerAuth> getRecentlyLoggedInPlayers();
/** /**
* Reload the data source. * Reload the data source.
*/ */

View File

@ -393,6 +393,11 @@ public class FlatFile implements DataSource {
throw new UnsupportedOperationException("Flat file no longer supported"); throw new UnsupportedOperationException("Flat file no longer supported");
} }
@Override
public List<PlayerAuth> getRecentlyLoggedInPlayers() {
throw new UnsupportedOperationException("Flat file no longer supported");
}
/** /**
* Creates a PlayerAuth object from the read data. * Creates a PlayerAuth object from the read data.
* *

View File

@ -716,6 +716,22 @@ public class MySQL implements DataSource {
return players; return players;
} }
@Override
public List<PlayerAuth> getRecentlyLoggedInPlayers() {
List<PlayerAuth> players = new ArrayList<>();
String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;";
try (Connection con = getConnection();
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
players.add(buildAuthFromResultSet(rs));
}
} catch (SQLException e) {
logSqlException(e);
}
return players;
}
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT); String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT);
int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP); int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP);

View File

@ -639,6 +639,20 @@ public class SQLite implements DataSource {
return players; return players;
} }
@Override
public List<PlayerAuth> getRecentlyLoggedInPlayers() {
List<PlayerAuth> players = new ArrayList<>();
String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;";
try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
players.add(buildAuthFromResultSet(rs));
}
} catch (SQLException e) {
logSqlException(e);
}
return players;
}
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;

View File

@ -50,6 +50,11 @@ public enum AdminPermission implements PermissionNode {
*/ */
GET_IP("authme.admin.getip"), GET_IP("authme.admin.getip"),
/**
* Administrator command to see the last recently logged in players.
*/
SEE_RECENT_PLAYERS("authme.admin.seerecent"),
/** /**
* Administrator command to teleport to the AuthMe spawn. * Administrator command to teleport to the AuthMe spawn.
*/ */

View File

@ -17,7 +17,7 @@ softdepend:
commands: commands:
authme: authme:
description: AuthMe op commands description: AuthMe op commands
usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|debug usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|debug
email: email:
description: Add email or recover password description: Add email or recover password
usage: /email show|add|change|recover|code|setpassword usage: /email show|add|change|recover|code|setpassword
@ -74,6 +74,7 @@ permissions:
authme.admin.register: true authme.admin.register: true
authme.admin.reload: true authme.admin.reload: true
authme.admin.seeotheraccounts: true authme.admin.seeotheraccounts: true
authme.admin.seerecent: true
authme.admin.setfirstspawn: true authme.admin.setfirstspawn: true
authme.admin.setspawn: true authme.admin.setspawn: true
authme.admin.spawn: true authme.admin.spawn: true
@ -134,6 +135,9 @@ permissions:
authme.admin.seeotheraccounts: authme.admin.seeotheraccounts:
description: Permission to see the other accounts of the players that log in. description: Permission to see the other accounts of the players that log in.
default: op default: op
authme.admin.seerecent:
description: Administrator command to see the last recently logged in players.
default: op
authme.admin.setfirstspawn: authme.admin.setfirstspawn:
description: Administrator command to set the first AuthMe spawn. description: Administrator command to set the first AuthMe spawn.
default: op default: op

View File

@ -0,0 +1,61 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import org.bukkit.command.CommandSender;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collections;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
/**
* Test for {@link RecentPlayersCommand}.
*/
@RunWith(MockitoJUnitRunner.class)
public class RecentPlayersCommandTest {
@InjectMocks
@Spy
private RecentPlayersCommand command;
@Mock
private DataSource dataSource;
@Test
public void shouldShowRecentPlayers() {
// given
PlayerAuth auth1 = PlayerAuth.builder()
.name("hannah").realName("Hannah").lastIp("11.11.11.11")
.lastLogin(1510387755000L) // 11/11/2017 @ 8:09am
.build();
PlayerAuth auth2 = PlayerAuth.builder()
.name("matt").realName("MATT").lastIp("22.11.22.33")
.lastLogin(1510269301000L) // 11/09/2017 @ 11:15pm
.build();
doReturn(ZoneId.of("UTC")).when(command).getZoneId();
given(dataSource.getRecentlyLoggedInPlayers()).willReturn(Arrays.asList(auth1, auth2));
CommandSender sender = mock(CommandSender.class);
// when
command.executeCommand(sender, Collections.emptyList());
// then
verify(sender).sendMessage(argThat(containsString("Recently logged in players")));
verify(sender).sendMessage("- Hannah (08:09 AM, 11 Nov with IP 11.11.11.11)");
verify(sender).sendMessage("- MATT (11:15 PM, 09 Nov with IP 22.11.22.33)");
}
}

View File

@ -1,5 +1,6 @@
package fr.xephi.authme.datasource; package fr.xephi.authme.datasource;
import com.google.common.collect.Lists;
import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.security.crypts.HashedPassword;
import org.junit.Test; import org.junit.Test;
@ -467,4 +468,30 @@ public abstract class AbstractDataSourceIntegrationTest {
assertThat(dataSource.hasSession("user"), equalTo(true)); assertThat(dataSource.hasSession("user"), equalTo(true));
assertThat(dataSource.hasSession("nonExistentName"), equalTo(false)); assertThat(dataSource.hasSession("nonExistentName"), equalTo(false));
} }
@Test
public void shouldGetRecentlyLoggedInPlayers() {
// given
DataSource dataSource = getDataSource();
String[] names = {"user3", "user8", "user2", "user4", "user7",
"user11", "user14", "user12", "user18", "user16",
"user28", "user29", "user22", "user20", "user24"};
long timestamp = 1461024000; // 2016-04-19 00:00:00
for (int i = 0; i < names.length; ++i) {
PlayerAuth auth = PlayerAuth.builder().name(names[i])
.registrationDate(1234567)
.lastLogin(timestamp + i * 3600)
.build();
dataSource.saveAuth(auth);
dataSource.updateSession(auth);
}
// when
List<PlayerAuth> recentPlayers = dataSource.getRecentlyLoggedInPlayers();
// then
assertThat(Lists.transform(recentPlayers, PlayerAuth::getNickname),
contains("user24", "user20", "user22", "user29", "user28",
"user16", "user18", "user12", "user14", "user11"));
}
} }