package net.frozenorb.apiv3.model; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.hash.Hashing; import com.mongodb.async.SingleResultCallback; import com.mongodb.async.client.MongoCollection; import com.mongodb.client.result.UpdateResult; import fr.javatic.mongo.jacksonCodec.Entity; import fr.javatic.mongo.jacksonCodec.objectId.Id; import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.serialization.gson.ExcludeFromReplies; import net.frozenorb.apiv3.serialization.jackson.UuidJsonDeserializer; import net.frozenorb.apiv3.serialization.jackson.UuidJsonSerializer; import net.frozenorb.apiv3.unsorted.BlockingCallback; import net.frozenorb.apiv3.util.MojangUtils; import net.frozenorb.apiv3.util.PermissionUtils; import net.frozenorb.apiv3.util.SyncUtils; import net.frozenorb.apiv3.util.UuidUtils; import org.bson.Document; import java.time.Instant; import java.util.*; @Entity @AllArgsConstructor public final class User { private static final MongoCollection usersCollection = APIv3.getDatabase().getCollection("users", User.class); @Getter @Id @JsonSerialize(using=UuidJsonSerializer.class) @JsonDeserialize(using=UuidJsonDeserializer.class) private UUID id; @Getter private String lastUsername; @Getter @ExcludeFromReplies private Map aliases = new HashMap<>(); @Getter @ExcludeFromReplies @Setter private String totpSecret; @Getter @ExcludeFromReplies @Setter private String emailToken; @Getter @ExcludeFromReplies @Setter private Instant emailTokenSetAt; @Getter @ExcludeFromReplies private String password; @Getter @Setter private String email; @Getter private String phoneNumber; @Getter private String lastSeenOn; @Getter private Instant lastSeenAt; @Getter private Instant firstSeenAt; @Getter private boolean online; public static User findByIdSync(String id) { UUID uuid; try { uuid = UUID.fromString(id); } catch (NullPointerException | IllegalArgumentException ex) { return null; } return findByIdSync(uuid); } public static User findByIdSync(UUID id) { if (UuidUtils.isAcceptableUuid(id)) { return SyncUtils.blockOne(usersCollection.find(new Document("_id", id))); } else { return null; } } public static User findByEmailTokenSync(String emailToken) { return SyncUtils.blockOne(usersCollection.find(new Document("emailToken", emailToken))); } public static User findByLastUsernameSync(String lastUsername) { return SyncUtils.blockOne(usersCollection.find(new Document("lastUsername", lastUsername))); } public static void findById(String id, SingleResultCallback callback) { try { UUID uuid = UUID.fromString(id); findById(uuid, callback); } catch (IllegalArgumentException ex) { // from UUID parsing callback.onResult(null, ex); } } public static void findById(UUID id, SingleResultCallback callback) { if (UuidUtils.isAcceptableUuid(id)) { usersCollection.find(new Document("_id", id)).first(callback); } else { callback.onResult(null, null); } } public static void findByIdGrouped(Iterable search, SingleResultCallback> callback) { usersCollection.find(new Document("_id", new Document("$in", search))).into(new ArrayList<>(), (users, error) -> { if (error != null) { callback.onResult(null, error); } else { Map result = new HashMap<>(); for (UUID user : search) { result.put(user, null); } for (User user : users) { result.put(user.getId(), user); } callback.onResult(result, null); } }); } public static void findByLastUsername(String lastUsername, SingleResultCallback callback) { usersCollection.find(new Document("lastUsername", lastUsername)).first(callback); } public User() {} // For Jackson // TODO: THIS IS CURRENTLY BLOCKING. MAYBE FOR THE HEARTBEAT WE CAN DO SOMETHING // TO MAKE IT NOT SO BLOCKING public User(UUID id, String lastUsername) { this.id = id; this.lastUsername = ""; // Intentional, so updateUsername actually does something. this.aliases = new HashMap<>(); this.lastSeenAt = Instant.now(); this.firstSeenAt = Instant.now(); updateUsername(lastUsername); } public boolean hasPermissionAnywhere(String permission) { Map globalPermissions = getGlobalPermissions(); return globalPermissions.containsKey(permission) && globalPermissions.get(permission); } // TODO: ASYNC public Map getGlobalPermissions() { Map globalPermissions = PermissionUtils.getDefaultPermissions(getHighestRankAnywhere()); for (Map.Entry serverGroupEntry : getHighestRanks().entrySet()) { ServerGroup serverGroup = serverGroupEntry.getKey(); Rank rank = serverGroupEntry.getValue(); globalPermissions = PermissionUtils.mergePermissions( globalPermissions, serverGroup.calculatePermissions(rank) ); } return ImmutableMap.copyOf(globalPermissions); } // TODO: Clean public boolean seenOnServer(Server server) { if (online && server.getId().equals(this.lastSeenOn)) { return false; } this.lastSeenOn = server.getId(); if (!online) { this.lastSeenAt = Instant.now(); } this.online = true; return true; } public void leftServer() { this.lastSeenAt = Instant.now(); this.online = false; } public void updateUsername(String newUsername) { if (!newUsername.equals(lastUsername)) { this.lastUsername = newUsername; User withNewUsername; while ((withNewUsername = User.findByLastUsernameSync(newUsername)) != null) { BlockingCallback callback = new BlockingCallback<>(); MojangUtils.getName(withNewUsername.getId(), callback); withNewUsername.updateUsername(callback.get()); } } this.aliases.put(newUsername, Instant.now()); } public void setPassword(String input) { this.password = Hashing .sha256() .hashString(input + "$" + id.toString(), Charsets.UTF_8) .toString(); } public boolean checkPassword(String input) { String hashed = Hashing .sha256() .hashString(input + "$" + id.toString(), Charsets.UTF_8) .toString(); return password != null && hashed.equals(password); } public Rank getHighestRankAnywhere() { return getHighestRankScoped(null, Grant.findByUserSync(this)); } // TODO: Clean // This is only used to help batch requests to mongo public Rank getHighestRankScoped(ServerGroup serverGroup, Iterable grants) { Rank highest = null; for (Grant grant : grants) { if (!grant.isActive() || (serverGroup != null && !grant.appliesOn(serverGroup))) { continue; } Rank rank = Rank.findById(grant.getRank()); if (highest == null || rank.getWeight() > highest.getWeight()) { highest = rank; } } if (highest != null) { return highest; } else { return Rank.findById("default"); } } // TODO: Clean public Map getHighestRanks() { Map highestRanks = new HashMap<>(); Rank defaultRank = Rank.findById("default"); List userGrants = Grant.findByUserSync(this); for (ServerGroup serverGroup : ServerGroup.findAll()) { Rank highest = defaultRank; for (Grant grant : userGrants) { if (!grant.isActive() || !grant.appliesOn(serverGroup)) { continue; } Rank rank = Rank.findById(grant.getRank()); if (highest == null || rank.getWeight() > highest.getWeight()) { highest = rank; } } highestRanks.put(serverGroup, highest); } return highestRanks; } public void getLoginInfo(Server server, String userIp, SingleResultCallback> callback) { Future> punishmentsFuture = Future.future(); Future> ipBansFuture = Future.future(); Future> grantsFuture = Future.future(); Punishment.findByUserAndType(this, ImmutableSet.of( Punishment.PunishmentType.BLACKLIST, Punishment.PunishmentType.BAN, Punishment.PunishmentType.MUTE ), (punishments, error) -> { if (error != null) { punishmentsFuture.fail(error); } else { punishmentsFuture.complete(punishments); } }); if (userIp != null) { IpBan.findByIp(userIp, (ipBans, error) -> { if (error != null) { ipBansFuture.fail(error); } else { ipBansFuture.complete(ipBans); } }); } else { ipBansFuture.complete(ImmutableSet.of()); } Grant.findByUser(this, (grants, error) -> { if (error != null) { grantsFuture.fail(error); } else { grantsFuture.complete(grants); } }); CompositeFuture.all(punishmentsFuture, ipBansFuture, grantsFuture).setHandler((result) -> { if (result.succeeded()) { Iterable punishments = result.result().result(0); Iterable ipBans = result.result().result(1); Iterable grants = result.result().result(2); callback.onResult(createLoginInfo(server, punishments, ipBans, grants), null); } else { callback.onResult(null, result.cause()); } }); } // This is only used to help batch requests to mongo public Map createLoginInfo(Server server, Iterable punishments, Iterable ipBans, Iterable grants) { Punishment activeMute = null; Punishment activeBan = null; IpBan activeIpBan = null; for (Punishment punishment : punishments) { if (!punishment.isActive()) { continue; } if (punishment.getType() == Punishment.PunishmentType.MUTE) { activeMute = punishment; } else if (punishment.getType() == Punishment.PunishmentType.BAN || punishment.getType() == Punishment.PunishmentType.BLACKLIST) { activeBan = punishment; } } for (IpBan ipBan : ipBans) { if (ipBan.isActive()) { activeIpBan = ipBan; break; } } Rank highestRank = getHighestRankScoped(ServerGroup.findById(server.getServerGroup()), grants); Map access = ImmutableMap.of( "allowed", true, "message", "Public server" ); if (activeBan != null) { access = ImmutableMap.of( "allowed", false, "message", activeBan.getAccessDenialReason(), "activeBanId", activeBan.getId() ); } else if (activeIpBan != null) { // TODO: ASYNC BlockingCallback callback = new BlockingCallback<>(); activeIpBan.getAccessDenialReason(callback); String reason = callback.get(); access = ImmutableMap.of( "allowed", false, "message", reason, "activeIpBanId", activeIpBan.getId() ); } // Generics are weird, yes we have to do this. ImmutableMap.Builder result = ImmutableMap.builder() .put("user", this) .put("access", access) .put("rank", highestRank.getId()) .put("totpSetup", getTotpSecret() != null); if (activeMute != null) { result.put("mute", activeMute); } return result.build(); } public void insert() { BlockingCallback callback = new BlockingCallback<>(); usersCollection.insertOne(this, callback); callback.get(); } public void save() { BlockingCallback callback = new BlockingCallback<>(); save(callback); callback.get(); } public void save(SingleResultCallback callback) { usersCollection.replaceOne(new Document("_id", id), this, callback); } }