diff --git a/API/.gitignore b/API/.gitignore
new file mode 100644
index 0000000..a2c5ea0
--- /dev/null
+++ b/API/.gitignore
@@ -0,0 +1,18 @@
+# IntelliJ
+.idea
+*.iml
+*.iws
+.idea\*.xml
+
+# Mac
+.DS_Store
+
+# Maven
+log/
+target/
+dependency-reduced-pom.xml
+
+# Java
+*.jar
+config.json
+disguisePresets.json
diff --git a/API/pom.xml b/API/pom.xml
new file mode 100644
index 0000000..ef35c7c
--- /dev/null
+++ b/API/pom.xml
@@ -0,0 +1,97 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.7.3
+
+
+
+ org.hcrival
+ api
+ 0.0.1-SNAPSHOT
+ api
+
+
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.google.guava
+ guava
+ 23.0
+ compile
+
+
+ org.mongodb
+ mongo-java-driver
+ LATEST
+ compile
+
+
+ javax.mail
+ mail
+ 1.5.0-b01
+ compile
+
+
+ com.warrenstrange
+ googleauth
+ 1.4.0
+ compile
+
+
+ org.apache.commons
+ commons-lang3
+ 3.5
+ compile
+
+
+ org.projectlombok
+ lombok
+ 1.18.24
+ compile
+
+
+ com.google.code.gson
+ gson
+ LATEST
+ compile
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ RELEASE
+ compile
+
+
+ redis.clients
+ jedis
+ 2.8.1
+ jar
+ compile
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/API/src/main/java/org/hcrival/api/InvictusAPI.java b/API/src/main/java/org/hcrival/api/InvictusAPI.java
new file mode 100644
index 0000000..c39b6f7
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/InvictusAPI.java
@@ -0,0 +1,104 @@
+package org.hcrival.api;
+
+import lombok.Getter;
+import org.hcrival.api.banphrase.BanphraseService;
+import org.hcrival.api.config.MainConfig;
+import org.hcrival.api.discord.DiscordService;
+import org.hcrival.api.disguise.DisguiseService;
+import org.hcrival.api.forum.ForumService;
+import org.hcrival.api.forum.account.AccountService;
+import org.hcrival.api.forum.category.CategoryService;
+import org.hcrival.api.forum.forum.ForumModel;
+import org.hcrival.api.forum.forum.ForumModelService;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.profile.ProfileController;
+import org.hcrival.api.profile.ProfileService;
+import org.hcrival.api.profile.grant.GrantService;
+import org.hcrival.api.profile.note.NoteService;
+import org.hcrival.api.punishment.PunishmentService;
+import org.hcrival.api.rank.RankService;
+import org.hcrival.api.redis.RedisService;
+import org.hcrival.api.tag.TagService;
+import org.hcrival.api.totp.TotpService;
+import org.hcrival.api.util.configuration.ConfigurationService;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+import java.io.File;
+import java.io.IOException;
+
+@SpringBootApplication
+@Getter
+public class InvictusAPI {
+
+ @Getter
+ private static InvictusAPI instance;
+
+ private final ConfigurationService configurationService = new JsonConfigurationService();
+ private final MainConfig mainConfig = configurationService.loadConfiguration(MainConfig.class, new File("./config.json"));
+
+ private final RedisService redisService;
+ private final MongoService mongoService;
+
+ private final TagService tagService;
+ private final RankService rankService;
+ private final PunishmentService punishmentService;
+ private final GrantService grantService;
+ private final NoteService noteService;
+ private final DisguiseService disguiseService;
+ private final BanphraseService banphraseService;
+ private final ProfileService profileService;
+ private final DiscordService discordService;
+ private final TotpService totpService;
+
+ // website
+ private final ForumService forumService;
+
+ private final long startedAt;
+
+ public InvictusAPI() {
+ InvictusAPI.instance = this;
+
+ this.redisService = new RedisService(mainConfig.getRedisConfig(), "invictus");
+ this.mongoService = new MongoService(this);
+ mongoService.connect();
+
+ this.discordService = new DiscordService(this);
+ this.profileService = new ProfileService(this);
+
+ this.tagService = new TagService(this);
+ tagService.loadTags();
+
+ this.rankService = new RankService(this);
+ rankService.loadRanks();
+
+ this.punishmentService = new PunishmentService(this);
+ this.grantService = new GrantService(this);
+ this.noteService = new NoteService(this);
+
+ this.disguiseService = new DisguiseService(this);
+ disguiseService.loadPresets();
+
+ this.banphraseService = new BanphraseService(this);
+ banphraseService.loadBanphrases();
+
+ this.totpService = new TotpService(this);
+ this.forumService = new ForumService(this);
+
+ this.startedAt = System.currentTimeMillis();
+ }
+
+ public void saveMainConfig() {
+ try {
+ configurationService.saveConfiguration(mainConfig, new File("./config.json"));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(InvictusAPI.class);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/MainController.java b/API/src/main/java/org/hcrival/api/MainController.java
new file mode 100644
index 0000000..4f21b41
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/MainController.java
@@ -0,0 +1,137 @@
+package org.hcrival.api;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import org.hcrival.api.util.JsonBuilder;
+import org.hcrival.api.util.TimeUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@RestController
+public class MainController {
+
+ private final InvictusAPI api = InvictusAPI.getInstance();
+
+ @GetMapping(path = "/istheapiworking")
+ public ResponseEntity test() {
+ return new ResponseEntity<>(new JsonBuilder().add("message", "yes!").build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/stats/cache")
+ public ResponseEntity statsCache() {
+ JsonBuilder response = new JsonBuilder();
+
+ JsonBuilder general = new JsonBuilder();
+ general.add("Loaded Ranks", api.getRankService().getCache().size());
+ general.add("Loaded Tags", api.getTagService().getCache().size());
+ general.add("Loaded Banphrases", api.getBanphraseService().getCache().size());
+ general.add("Cached Profiles", api.getProfileService().getCache().size());
+ general.add("Cached Punishments", api.getPunishmentService().getCache().size());
+ general.add("Cached Punishment Players", api.getPunishmentService().getPlayerCache().size());
+ general.add("Cached Grants", api.getGrantService().getCache().size());
+ general.add("Cached Grant Players", api.getGrantService().getPlayerCache().size());
+ general.add("Cached Disguise-Data", api.getDisguiseService().getCache().size());
+ general.add("Cached Discord-Data", api.getDiscordService().getUuidCache().size());
+
+ JsonBuilder forum = new JsonBuilder();
+ forum.add("Loaded Categories", api.getForumService().getCategoryService().getCache().size());
+ forum.add("Loaded Forums", api.getForumService().getForumModelService().getCache().size());
+ forum.add("Cached Accounts", api.getForumService().getAccountService().getCache().size());
+ forum.add("Cached Threads", api.getForumService().getThreadService().getCache().size());
+ forum.add("Cached Tickets", api.getForumService().getTicketService().getCache().size());
+
+ response.add("General", general.build());
+ response.add("Forums", forum.build());
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/stats/api")
+ public ResponseEntity statsApi() {
+ JsonBuilder response = new JsonBuilder();
+
+ response.add("Uptime", TimeUtils.formatTimeShort(System.currentTimeMillis() - api.getStartedAt()));
+// response.add("Handled Requests", master.getApi().getHandledRequests());
+
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/stats/master")
+ public ResponseEntity statsMaster() {
+ JsonBuilder response = new JsonBuilder();
+
+// response.add("Proxy Groups Loaded", api.getGroupService().getProxyGroups().size());
+// response.add("Server Groups Loaded", api.getGroupService().getServerGroups().size());
+// response.add("Static Servers Loaded", api.getGroupService().getStaticServers().size());
+//
+// response.add("Wrappers Connected", api.getWrapperTracker().getTracked().size());
+// response.add("Proxies Connected", api.getProxyTracker().getTracked().size());
+// response.add("Servers Connected", api.getServerTracker().getTracked().size());
+
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/stats/network")
+ public ResponseEntity statsNetwork() {
+ JsonBuilder response = new JsonBuilder();
+
+ response.add("Registered Players", api.getMongoService().getProfiles().countDocuments());
+// response.add("Player Peak (All Time)", api.getStatsTracker().getPlayerPeak());
+// response.add("Player Peak (Daily)", api.getStatsTracker().getDailyPlayerPeak());
+// response.add("Processed Logins (24h)", api.getStatsTracker().getProcessedLogins());
+// response.add("New players registered (24h)", api.getStatsTracker().getRegistrations());
+
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/stats/combined")
+ public ResponseEntity statsCombined() {
+ JsonBuilder response = new JsonBuilder();
+
+ response.add("cache", statsCache().getBody());
+ response.add("api", statsApi().getBody());
+ response.add("master", statsMaster().getBody());
+ response.add("network", statsNetwork().getBody());
+
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/oplist")
+ public ResponseEntity oplist() {
+ JsonArray oplist = new JsonArray();
+ api.getMainConfig().getOpList().forEach(uuid -> oplist.add(uuid.toString()));
+ return new ResponseEntity<>(oplist, HttpStatus.OK);
+ }
+
+ @PostMapping(path = "/oplist/{uuid}")
+ public ResponseEntity addToOpList(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ if (api.getMainConfig().getOpList().contains(uuid)) {
+ response.add("message", "Already on op list");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ api.getMainConfig().getOpList().add(uuid);
+ api.saveMainConfig();
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @DeleteMapping(path = "/oplist/{uuid}")
+ public ResponseEntity removeFromOplist(@PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ if (!api.getMainConfig().getOpList().contains(uuid)) {
+ response.add("message", "Not on op list");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ api.getMainConfig().getOpList().remove(uuid);
+ api.saveMainConfig();
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/banphrase/Banphrase.java b/API/src/main/java/org/hcrival/api/banphrase/Banphrase.java
new file mode 100644
index 0000000..c1d738c
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/banphrase/Banphrase.java
@@ -0,0 +1,56 @@
+package org.hcrival.api.banphrase;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class Banphrase {
+
+ private UUID id;
+ private String name;
+ private String phrase;
+ private String operator;
+ private String muteMode;
+ private long duration;
+ private boolean enabled;
+ private boolean caseSensitive;
+
+ public Banphrase(Document document) {
+ this.id = UUID.fromString(document.getString("id"));
+ this.name = document.getString("name");
+ this.phrase = document.getString("phrase");
+ this.operator = document.getString("operator");
+ this.muteMode = document.getString("muteMode");
+ this.duration = document.get("duration", Number.class).longValue();
+ this.enabled = document.getBoolean("enabled");
+ this.caseSensitive = document.getBoolean("caseSensitive");
+
+ if (!caseSensitive && !operator.equals("REGEX"))
+ this.phrase = phrase.toLowerCase();
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("id", id.toString());
+ document.append("name", name);
+ document.append("phrase", phrase);
+ document.append("operator", operator);
+ document.append("muteMode", muteMode);
+ document.append("duration", duration);
+ document.append("enabled", enabled);
+ document.append("caseSensitive", caseSensitive);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/banphrase/BanphraseController.java b/API/src/main/java/org/hcrival/api/banphrase/BanphraseController.java
new file mode 100644
index 0000000..805b40d
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/banphrase/BanphraseController.java
@@ -0,0 +1,118 @@
+package org.hcrival.api.banphrase;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Optional;
+import java.util.UUID;
+
+@RestController
+@RequestMapping(path = "/banphrase")
+public class BanphraseController {
+
+ private final InvictusAPI api;
+ private final BanphraseService banphraseService;
+
+ public BanphraseController(InvictusAPI api) {
+ this.api = api;
+ this.banphraseService = api.getBanphraseService();
+ }
+
+ @GetMapping
+ public ResponseEntity getBanphrases() {
+ JsonArray array = new JsonArray();
+ for (Banphrase banphrase : banphraseService.getBanphrases()) {
+ array.add(banphrase.toJson());
+ }
+ return new ResponseEntity<>(array, HttpStatus.OK);
+ }
+
+ @PostMapping
+ public ResponseEntity createBanphrase(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+
+ UUID id = body.has("id")
+ ? UUID.fromString(body.get("id").getAsString())
+ : UUID.randomUUID();
+
+ if (banphraseService.getBanphrase(id).isPresent()) {
+ response.add("message", "Banphrase already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ Banphrase banphrase = new Banphrase();
+ banphrase.setId(id);
+ banphrase.setName(body.get("name").getAsString());
+ banphrase.setPhrase(body.get("phrase").getAsString());
+ banphrase.setOperator(body.get("operator").getAsString());
+ banphrase.setMuteMode(body.get("muteMode").getAsString());
+ banphrase.setDuration(body.get("duration").getAsLong());
+ banphrase.setCaseSensitive(body.has("caseSensitive") && body.get("caseSensitive").getAsBoolean());
+ banphrase.setEnabled(!body.has("enabled") || body.get("enabled").getAsBoolean());
+
+ if (!banphrase.isCaseSensitive() && !banphrase.getOperator().equals("REGEX"))
+ banphrase.setPhrase(banphrase.getPhrase().toLowerCase());
+
+ banphraseService.saveBanphrase(banphrase);
+ return new ResponseEntity<>(banphrase.toJson(), HttpStatus.CREATED);
+ }
+
+ @PutMapping(path = "/{id}")
+ public ResponseEntity updateBanphrase(@RequestBody JsonObject body, @PathVariable(name = "id") UUID id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional banphraseOpt = banphraseService.getBanphrase(id);
+ if (!banphraseOpt.isPresent()) {
+ response.add("message", "Banphrase not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Banphrase banphrase = banphraseOpt.get();
+
+ if (body.has("name"))
+ banphrase.setName(body.get("name").getAsString());
+
+ if (body.has("caseSensitive"))
+ banphrase.setCaseSensitive(body.get("caseSensitive").getAsBoolean());
+
+ if (body.has("enabled"))
+ banphrase.setEnabled(body.get("enabled").getAsBoolean());
+
+ if (body.has("operator"))
+ banphrase.setOperator(body.get("operator").getAsString());
+
+ if (body.has("muteMode"))
+ banphrase.setMuteMode(body.get("muteMode").getAsString());
+
+ if (body.has("duration"))
+ banphrase.setDuration(body.get("duration").getAsLong());
+
+ if (body.has("phrase")) {
+ if (!banphrase.isCaseSensitive() && !banphrase.getOperator().equals("REGEX"))
+ banphrase.setPhrase(body.get("phrase").getAsString().toLowerCase());
+ else banphrase.setPhrase(body.get("phrase").getAsString());
+ }
+
+ banphraseService.saveBanphrase(banphrase);
+ return new ResponseEntity<>(banphrase.toJson(), HttpStatus.OK);
+ }
+
+ @DeleteMapping(path = "/{id}")
+ public ResponseEntity deleteBanphrase(@PathVariable(name = "id") UUID id) {
+ JsonBuilder response = new JsonBuilder();
+ if (!banphraseService.getBanphrase(id).isPresent()) {
+ response.add("message", "Banphrase not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Banphrase banphrase = banphraseService.deleteBanphrase(id);
+ return new ResponseEntity<>(banphrase.toJson(), HttpStatus.OK);
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/banphrase/BanphraseService.java b/API/src/main/java/org/hcrival/api/banphrase/BanphraseService.java
new file mode 100644
index 0000000..da60bc1
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/banphrase/BanphraseService.java
@@ -0,0 +1,54 @@
+package org.hcrival.api.banphrase;
+
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+@RequiredArgsConstructor
+public class BanphraseService {
+
+ private final InvictusAPI api;
+
+ @Getter
+ private final Map cache = new ConcurrentHashMap<>();
+
+ public void loadBanphrases() {
+ cache.clear();
+ api.getMongoService().getBanphrases().find()
+ .forEach((Block super Document>) document -> {
+ Banphrase banphrase = new Banphrase(document);
+ cache.put(banphrase.getId(), banphrase);
+ });
+ }
+
+ public List getBanphrases() {
+ return new ArrayList<>(cache.values());
+ }
+
+ public Optional getBanphrase(UUID id) {
+ return Optional.ofNullable(cache.get(id));
+ }
+
+ public void saveBanphrase(Banphrase banphrase) {
+ api.getMongoService().getBanphrases().replaceOne(
+ Filters.eq("id", banphrase.getId().toString()),
+ banphrase.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(banphrase.getId(), banphrase);
+ }
+
+ public Banphrase deleteBanphrase(UUID id) {
+ api.getMongoService().getBanphrases().deleteOne(Filters.eq("id", id.toString()));
+ return cache.remove(id);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/config/MainConfig.java b/API/src/main/java/org/hcrival/api/config/MainConfig.java
new file mode 100644
index 0000000..0af6b10
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/config/MainConfig.java
@@ -0,0 +1,21 @@
+package org.hcrival.api.config;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hcrival.api.util.configuration.StaticConfiguration;
+import org.hcrival.api.util.configuration.defaults.MongoConfig;
+import org.hcrival.api.util.configuration.defaults.RedisConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class MainConfig implements StaticConfiguration {
+
+ private final List opList = new ArrayList<>();
+ private RedisConfig redisConfig = new RedisConfig();
+ private MongoConfig mongoConfig = new MongoConfig();
+
+}
diff --git a/API/src/main/java/org/hcrival/api/discord/DiscordController.java b/API/src/main/java/org/hcrival/api/discord/DiscordController.java
new file mode 100644
index 0000000..2327eba
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/discord/DiscordController.java
@@ -0,0 +1,345 @@
+package org.hcrival.api.discord;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.profile.Profile;
+import org.hcrival.api.profile.ProfileService;
+import org.hcrival.api.profile.grant.Grant;
+import org.hcrival.api.profile.grant.GrantService;
+import org.hcrival.api.rank.RankService;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping(path = "/discord")
+public class DiscordController {
+
+ private final InvictusAPI api;
+ private final DiscordService discordService;
+ private final ProfileService profileService;
+ private final GrantService grantService;
+ private final RankService rankService;
+
+ public DiscordController(InvictusAPI api) {
+ this.api = api;
+ this.discordService = api.getDiscordService();
+ this.profileService = api.getProfileService();
+ this.grantService = api.getGrantService();
+ this.rankService = api.getRankService();
+ }
+
+
+ @GetMapping(path = "/hasboosted/{uuid}")
+ public ResponseEntity hasBoosted(@PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ Optional data = discordService.getByUuid(uuid);
+
+ boolean boosted = data.isPresent() && data.get().isBoosted();
+ response.add("boosted", boosted);
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/issynced/{uuid}")
+ public ResponseEntity isSynced(@PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ Optional data = discordService.getByUuid(uuid);
+
+ boolean synced = data.isPresent() && data.get().getMemberId() != null;
+ response.add("synced", synced);
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/{memberId}")
+ public ResponseEntity getMember(@PathVariable(name = "memberId") String memberId) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional dataOpt = discordService.getByMemberId(memberId);
+ if (!dataOpt.isPresent()) {
+ response.add("message", "Data not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ DiscordData data = dataOpt.get();
+ JsonObject dataJson = data.toJson();
+
+ Optional profileOpt = profileService.getProfile(data.getUuid());
+ if (!profileOpt.isPresent()) {
+ response.add("message", "Profile not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ dataJson.addProperty("displayName",
+ profileOpt.get().getRealCurrentGrant().asRank().getColor() + profileOpt.get().getName());
+ dataJson.addProperty("name", profileOpt.get().getName());
+
+
+ return new ResponseEntity<>(dataJson, HttpStatus.OK);
+ }
+
+ @PostMapping(path = "/ingame")
+ public ResponseEntity validateInGameSyncRequest(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+
+ UUID uuid = UUID.fromString(body.get("uuid").getAsString());
+ DiscordData data = discordService.getByUuid(uuid).orElse(null);
+
+ if (data != null && data.getMemberId() != null) {
+ response.add("alreadySynced", true);
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ if (data != null) {
+ response.add("code", data.getSyncCode());
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ String code = body.get("code").getAsString();
+ if (!discordService.isCodeAvailable(code)) {
+ response.add("message", "Code already in use");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ data = new DiscordData();
+ data.setUuid(uuid);
+ data.setSyncCode(code);
+ discordService.saveData(data);
+ return new ResponseEntity<>(response.build(), HttpStatus.CREATED);
+ }
+
+ @PutMapping(path = "/discord")
+ public ResponseEntity validateDiscordSyncRequest(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+ Optional dataOpt = discordService.getByCode(body.get("code").getAsString());
+
+ if (!dataOpt.isPresent()) {
+ response.add("message", "Discord Data not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ DiscordData data = dataOpt.get();
+ if (data.getMemberId() != null) {
+ response.add("invalidCode", true);
+ response.add("message", "Code already used");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ String memberId = body.get("memberId").getAsString();
+ if (discordService.getByMemberId(memberId).isPresent()) {
+ response.add("message", "Member already synced");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ data.setMemberId(memberId);
+ data.setBoosted(body.get("boosted").getAsBoolean());
+ discordService.saveData(data);
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @PutMapping
+ public ResponseEntity memberUpdate(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+ Optional dataOpt = discordService.getByMemberId(body.get("memberId").getAsString());
+
+ if (!dataOpt.isPresent()) {
+ response.add("message", "Discord Data not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ DiscordData data = dataOpt.get();
+
+ Optional profileOpt = profileService.getProfile(data.getUuid());
+ if (!profileOpt.isPresent()) {
+ response.add("message", "Profile not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ data.setBoosted(body.get("boosted").getAsBoolean());
+
+ Profile profile = profileOpt.get();
+
+ List currentDiscordRanks = new ArrayList<>();
+ List toRemove = new ArrayList<>();
+
+ body.get("ranks").getAsJsonArray().forEach(element -> currentDiscordRanks.add(element.getAsString()));
+ currentDiscordRanks.removeIf(rank -> !rankService.getByDiscordId(rank).isPresent());
+
+ List grants = grantService.getGrantsOf(profile.getUuid());
+
+ List allDiscordRanks = grants.stream()
+ .filter(grant -> grant.asRank().getDiscordId() != null
+ && grant.isActive()
+ && !grant.isRemoved())
+ .map(grant -> grant.asRank().getDiscordId())
+ .collect(Collectors.toList());
+
+ List toAdd = grants.stream()
+ .filter(grant -> grant.asRank().getDiscordId() != null
+ && !currentDiscordRanks.contains(grant.asRank().getDiscordId())
+ && grant.isActive()
+ && !grant.isRemoved())
+ .map(grant -> grant.asRank().getDiscordId())
+ .collect(Collectors.toList());
+
+ for (String s : currentDiscordRanks) {
+ boolean hasRank = grants.stream()
+ .anyMatch(grant -> grant.asRank().getDiscordId() != null
+ && grant.asRank().getDiscordId().equals(s)
+ && grant.isActive()
+ && !grant.isRemoved());
+
+ if (data.isRequestedRemoval() || (!hasRank && !allDiscordRanks.contains(s)))
+ toRemove.add(s);
+ }
+
+ if (data.isRequestedRemoval())
+ toAdd.clear();
+
+ {
+ JsonArray toAddArray = new JsonArray();
+ toAdd.forEach(toAddArray::add);
+ response.add("toAdd", toAddArray);
+
+ JsonArray toRemoveArray = new JsonArray();
+ toRemove.forEach(toRemoveArray::add);
+ response.add("toRemove", toRemoveArray);
+ }
+
+
+ allDiscordRanks.clear();
+ currentDiscordRanks.clear();
+ body.get("staffRanks").getAsJsonArray().forEach(element -> currentDiscordRanks.add(element.getAsString()));
+ toAdd.clear();
+ toRemove.clear();
+
+ allDiscordRanks.addAll(grants.stream()
+ .filter(grant -> grant.asRank().getDiscordId() != null
+ && grant.isActive()
+ && !grant.isRemoved())
+ .map(grant -> grant.asRank().getDiscordId())
+ .collect(Collectors.toList()));
+
+ toAdd.addAll(grants.stream()
+ .filter(grant -> grant.asRank().getStaffDiscordId() != null
+ && !currentDiscordRanks.contains(grant.asRank().getStaffDiscordId())
+ && grant.isActive()
+ && !grant.isRemoved())
+ .map(grant -> grant.asRank().getStaffDiscordId())
+ .collect(Collectors.toList()));
+
+ for (String s : currentDiscordRanks) {
+ boolean hasRank = grants.stream()
+ .anyMatch(grant -> grant.asRank().getStaffDiscordId() != null
+ && grant.asRank().getStaffDiscordId().equals(s)
+ && grant.isActive()
+ && !grant.isRemoved());
+
+ if (data.isRequestedRemoval() || (!hasRank && !allDiscordRanks.contains(s)))
+ toRemove.add(s);
+ }
+
+ if (data.isRequestedRemoval())
+ toAdd.clear();
+
+ currentDiscordRanks.removeIf(toRemove::contains);
+ boolean staffAccess = (!toAdd.isEmpty() || !currentDiscordRanks.isEmpty()) && !data.isRequestedRemoval();
+
+ {
+ JsonArray toAddArray = new JsonArray();
+ toAdd.forEach(toAddArray::add);
+ response.add("staffToAdd", toAddArray);
+
+ JsonArray toRemoveArray = new JsonArray();
+ toRemove.forEach(toRemoveArray::add);
+ response.add("staffToRemove", toRemoveArray);
+ }
+
+ response.add("requestedRemoval", data.isRequestedRemoval());
+ response.add("staffAccess", staffAccess);
+ response.add("nickname",
+ (profile.getRealCurrentGrant().asRank().getWeight() > 0
+ ? "[" + profile.getRealCurrentGrant().asRank().getName() + "] " : "")
+ + profile.getName());
+
+ discordService.saveData(data);
+
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/membersToUpdate")
+ public ResponseEntity getMembersToUpdate() {
+ JsonArray ids = new JsonArray();
+ discordService.getAllMemberIds().forEach(ids::add);
+ return new ResponseEntity<>(ids, HttpStatus.OK);
+ }
+
+ @PostMapping(path = "/requestRemoval/{uuid}")
+ public ResponseEntity requestRemoval(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ Optional dataOpt = discordService.getByUuid(uuid);
+
+ if (!dataOpt.isPresent()) {
+ response.add("message", "Discord Data not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ DiscordData data = dataOpt.get();
+ if (data.isRequestedRemoval()) {
+ response.add("message", "Already requested removal");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ data.setRequestedRemoval(true);
+ discordService.saveData(data);
+ return new ResponseEntity<>(data.toJson(), HttpStatus.OK);
+ }
+
+ @PutMapping(path = "/verifystaff")
+ public ResponseEntity verifyStaffAccess(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+ Optional dataOpt = discordService.getByMemberId(body.get("memberId").getAsString());
+
+ if (!dataOpt.isPresent()) {
+ response.add("message", "Discord Data not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ DiscordData data = dataOpt.get();
+
+ Optional profileOpt = profileService.getProfile(data.getUuid());
+ if (!profileOpt.isPresent()) {
+ response.add("message", "Profile not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Profile profile = profileOpt.get();
+ List grants = grantService.getGrantsOf(profile.getUuid()).stream()
+ .filter(grant -> grant.isActive()
+ && !grant.isRemoved()
+ && grant.asRank().getStaffDiscordId() != null)
+ .collect(Collectors.toList());
+
+ response.add("access", !grants.isEmpty());
+ response.add("nickname",
+ (profile.getRealCurrentGrant().asRank().getWeight() > 0
+ ? "[" + profile.getRealCurrentGrant().asRank().getName() + "] " : "")
+ + profile.getName());
+
+ JsonArray ranks = new JsonArray();
+ grants.forEach(grant -> ranks.add(grant.asRank().getStaffDiscordId()));
+ response.add("ranks", ranks);
+
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/discord/DiscordData.java b/API/src/main/java/org/hcrival/api/discord/DiscordData.java
new file mode 100644
index 0000000..a3af74f
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/discord/DiscordData.java
@@ -0,0 +1,51 @@
+package org.hcrival.api.discord;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.util.JsonBuilder;
+
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class DiscordData {
+
+ private UUID uuid;
+ private String memberId;
+ private String syncCode;
+ private boolean boosted;
+ private boolean requestedRemoval;
+
+ public DiscordData(Document document) {
+ this.uuid = UUID.fromString(document.getString("uuid"));
+ if (document.containsKey("memberId"))
+ this.memberId = document.getString("memberId");
+ this.syncCode = document.getString("syncCode");
+ this.boosted = document.getBoolean("boosted");
+ if (document.containsKey("requestedRemoval"))
+ this.requestedRemoval = document.getBoolean("requestedRemoval");
+ else requestedRemoval = false;
+ }
+
+ public Document toBson() {
+ return new Document()
+ .append("uuid", uuid.toString())
+ .append("memberId", memberId)
+ .append("syncCode", syncCode)
+ .append("boosted", boosted)
+ .append("requestedRemoval", requestedRemoval);
+ }
+
+ public JsonObject toJson() {
+ return new JsonBuilder()
+ .add("uuid", uuid.toString())
+ .add("memberId", memberId)
+ .add("syncCode", syncCode)
+ .add("boosted", boosted)
+ .add("requestedRemoval", requestedRemoval)
+ .build();
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/discord/DiscordService.java b/API/src/main/java/org/hcrival/api/discord/DiscordService.java
new file mode 100644
index 0000000..764829d
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/discord/DiscordService.java
@@ -0,0 +1,110 @@
+package org.hcrival.api.discord;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.util.exception.DataNotFoundException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+public class DiscordService {
+
+ private final InvictusAPI api;
+
+ @Getter
+ private final LoadingCache uuidCache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public DiscordData load(UUID uuid) throws DataNotFoundException {
+ Document document = api.getMongoService().getDiscordData()
+ .find(Filters.eq("uuid", uuid.toString())).first();
+
+ if (document == null)
+ throw new DataNotFoundException();
+
+ return new DiscordData(document);
+ }
+ });
+
+ private final LoadingCache memberIdCache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public DiscordData load(String memberId) throws DataNotFoundException {
+
+ Document document = api.getMongoService().getDiscordData()
+ .find(Filters.eq("memberId", memberId)).first();
+
+ if (document == null)
+ throw new DataNotFoundException();
+ return new DiscordData(document);
+ }
+ });
+
+ public Optional getByUuid(UUID uuid) {
+ try {
+ return Optional.ofNullable(uuidCache.get(uuid));
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public Optional getByMemberId(String memberId) {
+ try {
+ return Optional.ofNullable(memberIdCache.get(memberId));
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public void saveData(DiscordData data) {
+ api.getMongoService().getDiscordData().replaceOne(
+ Filters.eq("uuid", data.getUuid().toString()),
+ data.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ uuidCache.put(data.getUuid(), data);
+ if (data.getMemberId() != null)
+ memberIdCache.put(data.getMemberId(), data);
+ }
+
+ public Optional getByCode(String code) {
+ Document document = api.getMongoService().getDiscordData().find(Filters.eq("syncCode", code)).first();
+ if (document == null)
+ return Optional.empty();
+
+ return getByUuid(UUID.fromString(document.getString("uuid")));
+ }
+
+ public boolean isCodeAvailable(String code) {
+ return api.getMongoService().getDiscordData().find(Filters.eq("syncCode", code)).first() == null;
+ }
+
+ public List getAllMemberIds() {
+ List memberIds = new ArrayList<>();
+ for (Document document : api.getMongoService().getDiscordData().find()) {
+ if (document.containsKey("memberId") && document.get("memberId") != null)
+ memberIds.add(document.getString("memberId"));
+ }
+ return memberIds;
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/disguise/DisguiseController.java b/API/src/main/java/org/hcrival/api/disguise/DisguiseController.java
new file mode 100644
index 0000000..c49b0f9
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/disguise/DisguiseController.java
@@ -0,0 +1,98 @@
+package org.hcrival.api.disguise;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.util.JsonBuilder;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import javax.xml.ws.Response;
+import java.util.Optional;
+import java.util.UUID;
+
+@RestController
+@RequestMapping(path = "/disguise")
+public class DisguiseController {
+
+ private final InvictusAPI api;
+ private final DisguiseService disguiseService;
+
+ public DisguiseController(InvictusAPI api) {
+ this.api = api;
+ this.disguiseService = api.getDisguiseService();
+ }
+
+ @GetMapping(path = "/{uuid}")
+ public ResponseEntity getDisguiseData(@PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ Optional data = disguiseService.getData(uuid);
+
+ if (!data.isPresent()) {
+ response.add("message", "Disguise Data not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ return new ResponseEntity<>(data.get().toJson(), HttpStatus.OK);
+ }
+
+ @PostMapping
+ public ResponseEntity createDisguiseData(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+ DisguiseData data = JsonConfigurationService.gson.fromJson(body, DisguiseData.class);
+ if (disguiseService.getData(data.getUuid()).isPresent()) {
+ response.add("message", "Disguise Data already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ disguiseService.saveData(data);
+ return new ResponseEntity<>(data.toJson(), HttpStatus.CREATED);
+ }
+
+ @PutMapping
+ public ResponseEntity updateDisguiseData(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+ DisguiseData data = JsonConfigurationService.gson.fromJson(body, DisguiseData.class);
+ if (!disguiseService.getData(data.getUuid()).isPresent()) {
+ response.add("message", "Disguise Data not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ disguiseService.saveData(data);
+ return new ResponseEntity<>(data.toJson(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/{name}/available")
+ public ResponseEntity isNameAvailable(@PathVariable(name = "name") String name) {
+ JsonBuilder response = new JsonBuilder();
+ response.add("available", disguiseService.isNameAvailable(name));
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/{name}/namelogs")
+ public ResponseEntity getNameLogs(@PathVariable(name = "name") String name) {
+ JsonArray logs = new JsonArray();
+ disguiseService.getNameLogs(name).forEach(log -> logs.add(log.toJson()));
+ return new ResponseEntity<>(logs, HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/presets/names")
+ public ResponseEntity getNamePresets() {
+ JsonArray presets = new JsonArray();
+ disguiseService.getNamePresets().forEach(presets::add);
+ return new ResponseEntity<>(presets, HttpStatus.NOT_FOUND);
+ }
+
+ @GetMapping(path = "/presets/skins")
+ public ResponseEntity getSkinPresets() {
+ JsonArray presets = new JsonArray();
+ disguiseService.getSkinPresets().forEach(preset -> presets.add(preset.toJson()));
+ return new ResponseEntity<>(presets, HttpStatus.OK);
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/disguise/DisguiseData.java b/API/src/main/java/org/hcrival/api/disguise/DisguiseData.java
new file mode 100644
index 0000000..3af6cf2
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/disguise/DisguiseData.java
@@ -0,0 +1,61 @@
+package org.hcrival.api.disguise;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Data
+@NoArgsConstructor
+public class DisguiseData {
+
+ private UUID uuid;
+ private String disguiseName;
+ private UUID disguiseRank;
+ private String texture;
+ private String signature;
+ private List logs;
+
+ public DisguiseData(Document document) {
+ this.uuid = UUID.fromString(document.getString("uuid"));
+ this.disguiseName = document.getString("disguiseName");
+
+ if (document.containsKey("disguiseRank"))
+ this.disguiseRank = UUID.fromString(document.getString("disguiseRank"));
+
+ if (document.containsKey("texture"))
+ this.texture = document.getString("texture");
+
+ if (document.containsKey("signature"))
+ this.signature = document.getString("signature");
+
+ this.logs = document.getList("logs", Document.class).stream()
+ .map(DisguiseLogEntry::new)
+ .collect(Collectors.toList());
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("uuid", uuid.toString());
+ document.append("disguiseName", disguiseName);
+ document.append("disguiseNameLowerCase", disguiseName.toLowerCase());
+ document.append("disguiseRank", disguiseRank.toString());
+ document.append("texture", texture);
+ document.append("signature", signature);
+ document.append("logs", logs.stream()
+ .map(DisguiseLogEntry::toBson)
+ .collect(Collectors.toList()));
+ return document;
+ }
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/disguise/DisguiseLogEntry.java b/API/src/main/java/org/hcrival/api/disguise/DisguiseLogEntry.java
new file mode 100644
index 0000000..98bdd71
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/disguise/DisguiseLogEntry.java
@@ -0,0 +1,45 @@
+package org.hcrival.api.disguise;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class DisguiseLogEntry {
+
+ private UUID uuid;
+ private String name;
+ private String rank;
+ private long timeStamp;
+ private long removedAt;
+
+ public DisguiseLogEntry(Document document) {
+ this.uuid = UUID.fromString(document.getString("uuid"));
+ this.name = document.getString("name");
+ this.rank = document.getString("rank");
+ this.timeStamp = document.get("timeStamp", Number.class).longValue();
+ this.removedAt = document.get("removedAt", Number.class).longValue();
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("uuid", uuid.toString());
+ document.append("name", name);
+ document.append("nameLowerCase", name.toLowerCase());
+ document.append("rank", rank);
+ document.append("timeStamp", timeStamp);
+ document.append("removedAt", removedAt);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/disguise/DisguiseService.java b/API/src/main/java/org/hcrival/api/disguise/DisguiseService.java
new file mode 100644
index 0000000..803ce6e
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/disguise/DisguiseService.java
@@ -0,0 +1,98 @@
+package org.hcrival.api.disguise;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.disguise.config.DisguiseConfig;
+import org.hcrival.api.disguise.config.DisguiseSkinPreset;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.util.exception.DataNotFoundException;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@RequiredArgsConstructor
+public class DisguiseService {
+
+ private final InvictusAPI api;
+ private DisguiseConfig disguiseConfig;
+
+ @Getter
+ private final LoadingCache cache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public DisguiseData load(UUID uuid) throws DataNotFoundException {
+ Document document = api.getMongoService().getDisguiseData()
+ .find(Filters.eq("uuid", uuid.toString())).first();
+ if (document == null)
+ throw new DataNotFoundException();
+
+ return new DisguiseData(document);
+ }
+ });
+
+ public void loadPresets() {
+ disguiseConfig = api.getConfigurationService().loadConfiguration(DisguiseConfig.class,
+ new File("./disguisePresets.json"));
+ }
+
+ public Optional getData(UUID uuid) {
+ try {
+ return Optional.ofNullable(cache.get(uuid));
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public void saveData(DisguiseData data) {
+ api.getMongoService().getDisguiseData().replaceOne(
+ Filters.eq("uuid", data.getUuid().toString()),
+ data.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(data.getUuid(), data);
+ }
+
+ public boolean isNameAvailable(String name) {
+ return api.getMongoService().getDisguiseData().find(
+ Filters.eq("disguiseNameLowerCase", name.toLowerCase())).first() == null;
+ }
+
+ public List getNameLogs(String name) {
+ List toReturn = new ArrayList<>();
+ api.getMongoService().getDisguiseData().find(Filters.elemMatch("logs",
+ Filters.eq("nameLowerCase", name.toLowerCase())))
+ .forEach((Block super Document>) document ->
+ toReturn.addAll(document.getList("logs", Document.class).stream()
+ .map(DisguiseLogEntry::new)
+ .filter(log -> log.getName().equalsIgnoreCase(name))
+ .collect(Collectors.toList())));
+ return toReturn;
+ }
+
+ public List getNamePresets() {
+ return disguiseConfig.getNames();
+ }
+
+ public List getSkinPresets() {
+ return disguiseConfig.getSkins();
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/disguise/config/DisguiseConfig.java b/API/src/main/java/org/hcrival/api/disguise/config/DisguiseConfig.java
new file mode 100644
index 0000000..8172a2b
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/disguise/config/DisguiseConfig.java
@@ -0,0 +1,18 @@
+package org.hcrival.api.disguise.config;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hcrival.api.util.configuration.StaticConfiguration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+public class DisguiseConfig implements StaticConfiguration {
+
+ private List names = new ArrayList<>();
+
+ private List skins = new ArrayList<>();
+
+}
diff --git a/API/src/main/java/org/hcrival/api/disguise/config/DisguiseSkinPreset.java b/API/src/main/java/org/hcrival/api/disguise/config/DisguiseSkinPreset.java
new file mode 100644
index 0000000..92700d4
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/disguise/config/DisguiseSkinPreset.java
@@ -0,0 +1,23 @@
+package org.hcrival.api.disguise.config;
+
+import com.google.gson.JsonObject;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DisguiseSkinPreset {
+
+ private String name = "N/A";
+ private boolean hidden = false;
+ private String texture;
+ private String signature;
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/ForumService.java b/API/src/main/java/org/hcrival/api/forum/ForumService.java
new file mode 100644
index 0000000..035c2ff
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/ForumService.java
@@ -0,0 +1,40 @@
+package org.hcrival.api.forum;
+
+import lombok.Getter;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.forum.account.AccountService;
+import org.hcrival.api.forum.category.CategoryService;
+import org.hcrival.api.forum.forum.ForumModelService;
+import org.hcrival.api.forum.thread.ThreadService;
+import org.hcrival.api.forum.ticket.TicketService;
+import org.hcrival.api.forum.trophy.TrophyService;
+
+@Getter
+public class ForumService {
+
+ private final InvictusAPI api;
+
+ private final AccountService accountService;
+ private final CategoryService categoryService;
+ private final ForumModelService forumModelService;
+ private final ThreadService threadService;
+ private final TicketService ticketService;
+ private final TrophyService trophyService;
+
+ public ForumService(InvictusAPI api) {
+ this.api = api;
+ this.accountService = new AccountService(api);
+
+ this.categoryService = new CategoryService(api);
+ categoryService.loadCategories();
+
+ this.forumModelService = new ForumModelService(api);
+ forumModelService.loadForums();
+
+ this.threadService = new ThreadService(api);
+ this.ticketService = new TicketService(api);
+
+ this.trophyService = new TrophyService(api);
+ trophyService.loadTrophies();
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/account/AccountController.java b/API/src/main/java/org/hcrival/api/forum/account/AccountController.java
new file mode 100644
index 0000000..92b0f45
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/account/AccountController.java
@@ -0,0 +1,220 @@
+package org.hcrival.api.forum.account;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.forum.thread.ForumThread;
+import org.hcrival.api.profile.Profile;
+import org.hcrival.api.util.JsonBuilder;
+import org.hcrival.api.util.UUIDCache;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/forum/account")
+public class AccountController {
+
+ private final InvictusAPI api;
+
+ @PostMapping(path = "/sendRegistration/{uuid}")
+ public ResponseEntity sendRegistration(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional profileOpt = api.getProfileService().getProfile(uuid);
+ if (!profileOpt.isPresent()) {
+ response.add("message", "Profile not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Profile profile = profileOpt.get();
+
+ Optional accountOpt = api.getForumService().getAccountService().getAccount(uuid);
+ if (accountOpt.isPresent()) {
+ response.add("message", "Already registered");
+ response.add("registered", true);
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ accountOpt = api.getForumService().getAccountService().getByEmail(body.get("email").getAsString());
+ if (accountOpt.isPresent()) {
+ response.add("message", "Email in use");
+ response.add("emailInUse", true);
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ if (api.getForumService().getAccountService().getByToken(body.get("token").getAsString()).isPresent()) {
+ response.add("message", "Invalid token");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ ForumAccount account = new ForumAccount();
+ account.setUuid(profile.getUuid());
+ account.setEmail(body.get("email").getAsString().toLowerCase());
+ account.setToken(body.get("token").getAsString());
+ api.getForumService().getAccountService().saveAccount(account);
+
+ String format = String.format(
+ "http://70.34.207.5:8081/register?token=%s&email=%s&username=%s",
+ body.get("token").getAsString(),
+ body.get("email").getAsString(),
+ profile.getName()
+ );
+
+ System.out.println(format);
+
+// Optional playerOpt = api.getPlayerService().getPlayer(profile.getUuid());
+// playerOpt.ifPresent(player -> {
+// player.sendMessage(format);
+// });
+
+ response.add("message", "Confirmation email sent");
+ return new ResponseEntity<>(response.build(), HttpStatus.CREATED);
+
+ /*Properties properties = new Properties();
+ properties.put("mail.smtp.host", "mail.privateemail.com");
+ properties.put("mail.smtp.auth", "true");
+ properties.put("mail.smtp.port", "587");
+ properties.put("mail.smtp.starttls.enable", "true");
+ properties.put("mail.smtp.ssl.protocols", "TLSv1.2");
+
+ Session session = Session.getDefaultInstance(properties, new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication("noreply@arson.gg", "BCj$z!K@kn8soft?");
+ }
+ });
+
+ try {
+ MimeMessage message = new MimeMessage(session);
+ message.setFrom(new InternetAddress("noreply@arson.gg", "Brave.rip Network"));
+ message.addRecipient(Message.RecipientType.TO, new InternetAddress(body.get("email").getAsString()));
+ message.setSubject("Brave.rip - Complete Your Registration.");
+ message.setText(
+ "Hey, " + profile.getName() + "!\n\n" +
+ "Thanks for registering for an account on the Brave.rip Network. To complete your " +
+ "account registration, please click the following link:\n\n" +
+ String.format(
+ "http://brave.rip/register?token=%s&email=%s&username=%s",
+ body.get("token").getAsString(),
+ body.get("email").getAsString(),
+ profile.getName()
+ ) + "\n\n" +
+ "Thanks,\n" +
+ "The team at Brave.rip"
+ );
+
+ Transport.send(message);
+
+ } catch (MessagingException | UnsupportedEncodingException e) {
+ e.printStackTrace();
+ response.add("message", e.getClass().getSimpleName() + " when sending email");
+ return new Response(Status.INTERNAL_ERROR, response.build());
+ }*/
+ }
+
+ @GetMapping(path = "/threads/{uuid}")
+ public ResponseEntity getThreads(@PathVariable(name = "uuid") UUID uuid) {
+ JsonArray array = new JsonArray();
+ List profileThreads = api.getForumService().getThreadService()
+ .getProfileThreads(uuid, 0);
+
+ for (ForumThread profileThread : profileThreads)
+ array.add(profileThread.toJson());
+
+ return new ResponseEntity<>(array, HttpStatus.OK);
+ }
+
+ @PostMapping(path = "/register")
+ public ResponseEntity register(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+
+ String token = body.get("token").getAsString();
+ Optional accountOpt = api.getForumService().getAccountService().getByToken(token);
+ if (!accountOpt.isPresent()) {
+ response.add("message", "Token not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumAccount account = accountOpt.get();
+
+ if (account.getPassword() != null) {
+ response.add("message", "Token expired");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ account.setPassword(body.get("password").getAsString());
+ api.getForumService().getAccountService().saveAccount(account);
+
+ return new ResponseEntity<>(response.build(), HttpStatus.CREATED);
+ }
+
+ @GetMapping(path = "/login/{username}")
+ public ResponseEntity login(@PathVariable(name = "username") String username) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional accountOpt;
+ if (username.contains("@"))
+ accountOpt = api.getForumService().getAccountService().getByEmail(username);
+ else {
+ UUID uuid = UUIDCache.getUuid(username);
+ if (uuid == null)
+ accountOpt = Optional.empty();
+ else accountOpt = api.getForumService().getAccountService().getAccount(uuid);
+ }
+
+ if (!accountOpt.isPresent()) {
+ response.add("message", "Account not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ return new ResponseEntity<>(accountOpt.get().toJson(), HttpStatus.OK);
+ }
+
+ @PutMapping(path = "/setting/{uuid}")
+ public ResponseEntity updateSetting(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional accountOpt = api.getForumService().getAccountService().getAccount(uuid);
+ if (!accountOpt.isPresent()) {
+ response.add("message", "Account not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumAccount account = accountOpt.get();
+ body.keySet().forEach(key -> account.getSettings().put(key, body.get(key).getAsString()));
+ api.getForumService().getAccountService().saveAccount(account);
+ return new ResponseEntity<>(account.toJson(), HttpStatus.CREATED);
+ }
+
+ @PutMapping(path = "/password/{uuid}")
+ public ResponseEntity updatePassword(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional accountOpt = api.getForumService().getAccountService().getAccount(uuid);
+ if (!accountOpt.isPresent()) {
+ response.add("message", "Account not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumAccount account = accountOpt.get();
+ if (!account.getPassword().equals(body.get("currentPassword").getAsString())) {
+ response.add("message", "Invalid password");
+ response.add("invalidPassword", true);
+ return new ResponseEntity<>(response.build(), HttpStatus.FORBIDDEN);
+ }
+
+ account.setPassword(body.get("password").getAsString());
+ api.getForumService().getAccountService().saveAccount(account);
+ return new ResponseEntity<>(account.toJson(), HttpStatus.OK);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/account/AccountService.java b/API/src/main/java/org/hcrival/api/forum/account/AccountService.java
new file mode 100644
index 0000000..8ee1416
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/account/AccountService.java
@@ -0,0 +1,80 @@
+package org.hcrival.api.forum.account;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.util.exception.DataNotFoundException;
+
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+public class AccountService {
+
+ private final InvictusAPI api;
+
+ @Getter
+ private final LoadingCache cache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public ForumAccount load(UUID uuid) throws DataNotFoundException {
+ Document document = api.getMongoService().getForumAccounts()
+ .find(Filters.eq("uuid", uuid.toString())).first();
+ if (document == null)
+ throw new DataNotFoundException();
+
+ return new ForumAccount(document);
+ }
+ });
+
+ public Optional getByToken(String token) {
+ Document document = api.getMongoService().getForumAccounts().find(Filters.eq("token", token)).first();
+ if (document == null)
+ return Optional.empty();
+
+ return Optional.of(new ForumAccount(document));
+ }
+
+ public Optional getByEmail(String email) {
+ Document document = api.getMongoService().getForumAccounts().find(Filters.eq(
+ "email",
+ email.toLowerCase()
+ )).first();
+
+ if (document == null)
+ return Optional.empty();
+
+ return Optional.of(new ForumAccount(document));
+ }
+
+ public Optional getAccount(UUID uuid) {
+ try {
+ return Optional.ofNullable(cache.get(uuid));
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public void saveAccount(ForumAccount account) {
+ api.getMongoService().getForumAccounts().replaceOne(
+ Filters.eq("uuid", account.getUuid().toString()),
+ account.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(account.getUuid(), account);
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/account/ForumAccount.java b/API/src/main/java/org/hcrival/api/forum/account/ForumAccount.java
new file mode 100644
index 0000000..29e45db
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/account/ForumAccount.java
@@ -0,0 +1,55 @@
+package org.hcrival.api.forum.account;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class ForumAccount {
+
+ private UUID uuid;
+ private String email;
+ private String password;
+ private String token;
+
+ private Map settings = new HashMap<>();
+
+ public ForumAccount(Document document) {
+ this.uuid = UUID.fromString(document.getString("uuid"));
+ this.email = document.getString("email");
+ this.password = document.getString("password");
+ this.token = document.getString("token");
+
+ Document settingsDocument = document.containsKey("settings")
+ ? document.get("settings", Document.class)
+ : new Document();
+
+ settingsDocument.keySet().forEach(key -> settings.put(key, settingsDocument.getString(key)));
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("uuid", uuid.toString());
+ document.append("email", email);
+ document.append("password", password);
+ document.append("token", token);
+
+ Document settingsDocument = new Document();
+ settings.forEach(settingsDocument::append);
+ document.append("settings", settingsDocument);
+
+ return document;
+ }
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/category/CategoryController.java b/API/src/main/java/org/hcrival/api/forum/category/CategoryController.java
new file mode 100644
index 0000000..60abc68
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/category/CategoryController.java
@@ -0,0 +1,101 @@
+package org.hcrival.api.forum.category;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Optional;
+
+@RestController
+@RequestMapping(path = "/forum/category")
+public class CategoryController {
+
+ private final InvictusAPI api;
+ private final CategoryService categoryService;
+
+ public CategoryController(InvictusAPI api) {
+ this.api = api;
+ this.categoryService = this.api.getForumService().getCategoryService();
+ }
+
+ @GetMapping
+ public ResponseEntity getCategories() {
+ JsonArray array = new JsonArray();
+ api.getForumService().getCategoryService().getCategories().forEach(category -> array.add(category.toJson()));
+ return new ResponseEntity<>(array, HttpStatus.OK);
+ }
+
+ @PostMapping
+ public ResponseEntity createCategory(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+
+ String id = body.get("id").getAsString();
+ String name = body.get("name").getAsString();
+
+ Optional categoryOpt = categoryService.getByIdOrName(id);
+ if (categoryOpt.isPresent()) {
+ response.add("message", "Category already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ categoryOpt = categoryService.getByIdOrName(name);
+ if (categoryOpt.isPresent()) {
+ response.add("message", "Category already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ ForumCategory category = new ForumCategory();
+ category.setId(id);
+ category.setName(name);
+ category.setWeight(body.get("weight").getAsInt());
+
+ categoryService.saveCategory(category);
+
+ return new ResponseEntity<>(category.toJson(), HttpStatus.CREATED);
+ }
+
+ @PutMapping(path = "/{id}")
+ public ResponseEntity updateCategory(@RequestBody JsonObject body, @PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional categoryOpt = categoryService.getById(id);
+ if (!categoryOpt.isPresent()) {
+ response.add("message", "Category not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumCategory category = categoryOpt.get();
+ if (body.has("name"))
+ category.setName(body.get("name").getAsString());
+
+ if (body.has("weight"))
+ category.setWeight(body.get("weight").getAsInt());
+
+ categoryService.saveCategory(category);
+
+ return new ResponseEntity<>(category.toJson(), HttpStatus.OK);
+ }
+
+ @DeleteMapping(path = "/{id}")
+ public ResponseEntity deleteCategory(@PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional categoryOpt = categoryService.getById(id);
+ if (!categoryOpt.isPresent()) {
+ response.add("message", "Category not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumCategory category = categoryOpt.get();
+ categoryService.deleteCategory(category);
+ return new ResponseEntity<>(category.toJson(), HttpStatus.OK);
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/category/CategoryService.java b/API/src/main/java/org/hcrival/api/forum/category/CategoryService.java
new file mode 100644
index 0000000..dd8a717
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/category/CategoryService.java
@@ -0,0 +1,72 @@
+package org.hcrival.api.forum.category;
+
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+@RequiredArgsConstructor
+public class CategoryService {
+
+ private final InvictusAPI master;
+
+ @Getter
+ private final Map cache = new ConcurrentHashMap<>();
+
+ public void loadCategories() {
+ master.getMongoService().getForumCategories().find().forEach((Consumer super Document>) document -> {
+ ForumCategory category = new ForumCategory(document);
+ cache.put(category.getId(), category);
+ });
+ }
+
+ public Optional getById(String id) {
+ return Optional.ofNullable(cache.get(id));
+ }
+
+ public Optional getByName(String name) {
+ for (ForumCategory category : cache.values()) {
+ if (category.getName().equalsIgnoreCase(name))
+ return Optional.of(category);
+ }
+
+ return Optional.empty();
+ }
+
+ public Optional getByIdOrName(String key) {
+ Optional optional = getById(key);
+ if (optional.isPresent())
+ return optional;
+
+ return getByName(key);
+ }
+
+ public List getCategories() {
+ return new ArrayList<>(cache.values());
+ }
+
+ public void saveCategory(ForumCategory category) {
+ master.getMongoService().getForumCategories().replaceOne(
+ Filters.eq("id", category.getId()),
+ category.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(category.getId(), category);
+ }
+
+ public void deleteCategory(ForumCategory category) {
+ master.getMongoService().getForumCategories().deleteOne(Filters.eq("id", category.getId()));
+ cache.remove(category.getId());
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/category/ForumCategory.java b/API/src/main/java/org/hcrival/api/forum/category/ForumCategory.java
new file mode 100644
index 0000000..d452d20
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/category/ForumCategory.java
@@ -0,0 +1,45 @@
+package org.hcrival.api.forum.category;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.forum.forum.ForumModel;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+@Data
+@NoArgsConstructor
+public class ForumCategory {
+
+ private String id;
+ private String name;
+ private int weight;
+
+ public ForumCategory(Document document) {
+ this.id = document.getString("id");
+ this.name = document.getString("name");
+ this.weight = document.getInteger("weight");
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("id", id);
+ document.append("name", name);
+ document.append("weight", weight);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ JsonObject object = JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+
+ JsonArray array = new JsonArray();
+ for (ForumModel forum : InvictusAPI.getInstance().getForumService().getForumModelService().getByCategory(id))
+ array.add(forum.toJson());
+ object.add("forums", array);
+
+ return object;
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/forum/ForumModel.java b/API/src/main/java/org/hcrival/api/forum/forum/ForumModel.java
new file mode 100644
index 0000000..70d1d3f
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/forum/ForumModel.java
@@ -0,0 +1,66 @@
+package org.hcrival.api.forum.forum;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.forum.category.ForumCategory;
+import org.hcrival.api.forum.thread.ForumThread;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+@Data
+@NoArgsConstructor
+public class ForumModel {
+
+ private String id;
+ private String name;
+ private String description;
+ private int weight;
+ private boolean locked;
+ private String category;
+
+ public ForumModel(Document document) {
+ this.id = document.getString("id");
+ this.name = document.getString("name");
+ this.description = document.getString("description");
+ this.weight = document.getInteger("weight");
+ this.locked = document.getBoolean("locked");
+ this.category = document.getString("category");
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("id", id);
+ document.append("name", name);
+ document.append("description", description);
+ document.append("weight", weight);
+ document.append("locked", locked);
+ document.append("category", category);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ JsonObject object = JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+
+ if (getParent() != null) {
+ object.addProperty("categoryName", getParent().getName());
+ object.addProperty("categoryWeight", getParent().getWeight());
+ }
+
+ ForumThread thread = InvictusAPI.getInstance().getForumService()
+ .getForumModelService().getLastThread(this).orElse(null);
+
+ if (thread != null)
+ object.add("lastThread", thread.toJson());
+
+ long threadAmount = InvictusAPI.getInstance().getForumService().getForumModelService().threadSize(this);
+ object.addProperty("threadAmount", threadAmount);
+
+ return object;
+ }
+
+ public ForumCategory getParent() {
+ return InvictusAPI.getInstance().getForumService().getCategoryService().getById(category).orElse(null);
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/forum/ForumModelController.java b/API/src/main/java/org/hcrival/api/forum/forum/ForumModelController.java
new file mode 100644
index 0000000..4e978cd
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/forum/ForumModelController.java
@@ -0,0 +1,119 @@
+package org.hcrival.api.forum.forum;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.forum.thread.ForumThread;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Optional;
+
+@RestController
+@RequestMapping(path = "/forum/forum")
+public class ForumModelController {
+
+ private final InvictusAPI api;
+ private final ForumModelService forumService;
+
+ public ForumModelController(InvictusAPI api) {
+ this.api = api;
+ this.forumService = api.getForumService().getForumModelService();
+ }
+
+ @PostMapping
+ public ResponseEntity createForum(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+
+ String id = body.get("id").getAsString();
+ Optional forumOpt = forumService.getById(id);
+ if (forumOpt.isPresent()) {
+ response.add("message", "Forum already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ ForumModel forum = new ForumModel();
+ forum.setId(id);
+ forum.setName(body.get("name").getAsString());
+ forum.setDescription(body.get("description").getAsString());
+ forum.setWeight(body.get("weight").getAsInt());
+ forum.setLocked(body.get("locked").getAsBoolean());
+ forum.setCategory(body.get("categoryId").getAsString());
+
+ forumService.saveForum(forum);
+ return new ResponseEntity<>(forum.toJson(), HttpStatus.CREATED);
+ }
+
+ @PutMapping(path = "/{id}")
+ public ResponseEntity editForum(@RequestBody JsonObject body, @PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional forumOpt = forumService.getById(id);
+ if (!forumOpt.isPresent()) {
+ response.add("message", "Forum not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumModel forum = forumOpt.get();
+
+ if (body.has("name"))
+ forum.setName(body.get("name").getAsString());
+
+ if (body.has("description"))
+ forum.setDescription(body.get("description").getAsString());
+
+ if (body.has("weight"))
+ forum.setWeight(body.get("weight").getAsInt());
+
+ if (body.has("locked"))
+ forum.setLocked(body.get("locked").getAsBoolean());
+
+ forumService.saveForum(forum);
+ return new ResponseEntity<>(forum.toJson(), HttpStatus.OK);
+ }
+
+ @DeleteMapping(path = "/{id}")
+ public ResponseEntity deleteForum(@PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional forumOpt = forumService.getById(id);
+ if (!forumOpt.isPresent()) {
+ response.add("message", "Forum not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumModel forum = forumOpt.get();
+ forumService.deleteForum(forum);
+ return new ResponseEntity<>(forum.toJson(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/{id}")
+ public ResponseEntity getForum(@PathVariable(name = "id") String id,
+ @RequestParam(name = "page", defaultValue = "-1") int page) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional forumOpt = forumService.getByIdOrName(id.replace("-", " "));
+ if (!forumOpt.isPresent()) {
+ response.add("message", "Forum not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumModel forum = forumOpt.get();
+ JsonObject object = forum.toJson();
+
+ if (page > 0) {
+ List threads = api.getForumService().getThreadService().getForumThreads(forum.getId(), page);
+ JsonArray array = new JsonArray();
+ threads.forEach(thread -> array.add(thread.toJson()));
+ object.add("threads", array);
+ }
+
+ return new ResponseEntity<>(object, HttpStatus.OK);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/forum/ForumModelService.java b/API/src/main/java/org/hcrival/api/forum/forum/ForumModelService.java
new file mode 100644
index 0000000..9eee558
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/forum/ForumModelService.java
@@ -0,0 +1,103 @@
+package org.hcrival.api.forum.forum;
+
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.Sorts;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.forum.thread.ForumThread;
+import org.hcrival.api.mongo.MongoService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@RequiredArgsConstructor
+public class ForumModelService {
+
+ private final InvictusAPI master;
+
+ @Getter
+ private final Map cache = new ConcurrentHashMap<>();
+
+ public void loadForums() {
+ master.getMongoService().getForumForums().find().forEach((Block super Document>) document -> {
+ ForumModel forum = new ForumModel(document);
+ cache.put(forum.getId(), forum);
+ });
+ }
+
+ public Optional getById(String id) {
+ return Optional.ofNullable(cache.get(id));
+ }
+
+ public Optional getByName(String name) {
+ for (ForumModel forum : cache.values()) {
+ if (forum.getName().equalsIgnoreCase(name))
+ return Optional.of(forum);
+ }
+
+ return Optional.empty();
+ }
+
+ public Optional getByIdOrName(String key) {
+ Optional optional = getById(key);
+ if (optional.isPresent())
+ return optional;
+
+ return getByName(key);
+ }
+
+ public List getForums() {
+ return new ArrayList<>(cache.values());
+ }
+
+ public void saveForum(ForumModel forum) {
+ master.getMongoService().getForumForums().replaceOne(
+ Filters.eq("id", forum.getId()),
+ forum.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(forum.getId(), forum);
+ }
+
+ public void deleteForum(ForumModel forum) {
+ master.getMongoService().getForumForums().deleteOne(Filters.eq("id", forum.getId()));
+ cache.remove(forum.getId());
+ }
+
+ public List getByCategory(String categoryId) {
+ List forums = new ArrayList<>();
+ for (ForumModel forum : getForums()) {
+ if (forum.getCategory().equalsIgnoreCase(categoryId))
+ forums.add(forum);
+ }
+
+ return forums;
+ }
+
+ public Optional getLastThread(ForumModel model) {
+ Document document = master.getMongoService().getForumThreads().find(Filters.and(
+ Filters.eq("parentThreadId", null),
+ Filters.eq("forum", model.getId())
+ )).sort(Sorts.ascending("createdAt")).first();
+
+ if (document == null)
+ return Optional.empty();
+
+ return Optional.of(new ForumThread(document));
+ }
+
+ public long threadSize(ForumModel model) {
+ return master.getMongoService().getForumThreads().countDocuments(Filters.and(
+ Filters.eq("parentThreadId", null),
+ Filters.eq("forum", model.getId())
+ ));
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/search/SearchController.java b/API/src/main/java/org/hcrival/api/forum/search/SearchController.java
new file mode 100644
index 0000000..462ffa4
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/search/SearchController.java
@@ -0,0 +1,46 @@
+package org.hcrival.api.forum.search;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.regex.Pattern;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/search")
+public class SearchController {
+
+ private final InvictusAPI master;
+
+ @GetMapping
+ public ResponseEntity search(@RequestParam(name = "query") String query,
+ @RequestParam(name = "limit", defaultValue = "6") int limit) {
+ JsonArray array = new JsonArray();
+
+ Pattern pattern = Pattern.compile("^" + Pattern.quote(query), Pattern.CASE_INSENSITIVE);
+ master.getMongoService().getProfiles().find(Filters.regex("name", pattern))
+ .limit(limit)
+ .forEach((Block super Document>) document -> {
+ JsonObject object = new JsonObject();
+ object.addProperty("name", document.getString("name"));
+ object.addProperty("uuid", document.getString("uuid"));
+ array.add(object);
+ });
+
+ return new ResponseEntity<>(array, HttpStatus.OK);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/thread/ForumThread.java b/API/src/main/java/org/hcrival/api/forum/thread/ForumThread.java
new file mode 100644
index 0000000..ab3d569
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/thread/ForumThread.java
@@ -0,0 +1,123 @@
+package org.hcrival.api.forum.thread;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.forum.forum.ForumModel;
+import org.hcrival.api.profile.Profile;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class ForumThread {
+
+ private String id;
+
+ private String title;
+ private String body;
+
+ private String forum;
+ private UUID author;
+ private long createdAt;
+
+ private UUID lastEditedBy;
+ private long lastEditedAt;
+
+ private long lastReplyAt;
+
+ private boolean pinned;
+ private boolean locked;
+
+ private String parentThreadId;
+ private transient List replies = new ArrayList<>();
+
+ public ForumThread(Document document) {
+ this.id = document.getString("id");
+ this.title = document.getString("title");
+ this.body = document.getString("body");
+ this.forum = document.getString("forum");
+ this.author = UUID.fromString(document.getString("author"));
+ this.createdAt = document.getLong("createdAt");
+ this.lastEditedBy = document.getString("lastEditedBy") != null
+ ? UUID.fromString(document.getString("lastEditedBy")) : null;
+ this.lastEditedAt = document.getLong("lastEditedAt");
+ this.lastReplyAt = document.getLong("lastReplyAt");
+ this.pinned = document.getBoolean("pinned");
+ this.locked = document.containsKey("locked") ? document.getBoolean("locked") : false;
+ this.parentThreadId = document.getString("parentThreadId");
+
+ document.getList("replies", Document.class)
+ .forEach(replyDocument -> replies.add(new ForumThread(replyDocument)));
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("id", id);
+ document.append("title", title);
+ document.append("body", body);
+ document.append("forum", forum);
+ document.append("author", author.toString());
+ document.append("createdAt", createdAt);
+ document.append("lastEditedBy", lastEditedBy == null ? null : lastEditedBy.toString());
+ document.append("lastEditedAt", lastEditedAt);
+ document.append("lastReplyAt", lastReplyAt);
+ document.append("pinned", pinned);
+ document.append("locked", locked);
+ document.append("parentThreadId", parentThreadId);
+
+ List documents = new ArrayList<>();
+ replies.forEach(reply -> documents.add(reply.toBson()));
+ document.append("replies", documents);
+
+ return document;
+ }
+
+ public JsonObject toJson() {
+ JsonObject object = JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+
+ Profile profile = InvictusAPI.getInstance().getProfileService().getProfile(author).orElse(null);
+ if (profile != null) {
+ object.addProperty("authorName", profile.getName());
+ object.addProperty("authorWebColor", profile.getRealCurrentGrant().asRank().getWebColor());
+ }
+
+ ForumModel forum = getParent();
+ if (forum != null)
+ object.addProperty("forumName", forum.getName());
+
+ if (lastEditedBy != null) {
+ profile = InvictusAPI.getInstance().getProfileService().getProfile(lastEditedBy).orElse(null);
+
+ if (profile != null) {
+ object.addProperty("lastEditedByName", profile.getName());
+ object.addProperty("lastEditedByWebColor", profile.getRealCurrentGrant().asRank().getWebColor());
+ }
+ }
+
+ JsonArray array = new JsonArray();
+ replies.forEach(reply -> array.add(reply.toJson()));
+ object.add("replies", array);
+
+ return object;
+ }
+
+ public ForumModel getParent() {
+ return forum != null
+ ? InvictusAPI.getInstance().getForumService().getForumModelService().getById(forum).orElse(null)
+ : null;
+ }
+
+ public ForumThread getParentThread() {
+ return parentThreadId != null
+ ? InvictusAPI.getInstance().getForumService().getThreadService().getThread(parentThreadId).orElse(null)
+ : null;
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/thread/ThreadController.java b/API/src/main/java/org/hcrival/api/forum/thread/ThreadController.java
new file mode 100644
index 0000000..837ac34
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/thread/ThreadController.java
@@ -0,0 +1,208 @@
+package org.hcrival.api.forum.thread;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.protocol.ResponseDate;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.forum.forum.ForumModel;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RestController
+@RequestMapping(path = "/forum/thread")
+public class ThreadController {
+
+ private final InvictusAPI api;
+ private final ThreadService threadService;
+
+ public ThreadController(InvictusAPI api) {
+ this.api = api;
+ this.threadService = api.getForumService().getThreadService();
+ }
+
+ @PostMapping
+ public ResponseEntity createThread(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+
+ String id = body.get("id").getAsString();
+ Optional threadOpt = threadService.getThread(id);
+ if (threadOpt.isPresent()) {
+ response.add("message", "Thread already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ ForumThread thread = new ForumThread();
+ thread.setId(id);
+ thread.setTitle(body.get("title").getAsString());
+ thread.setBody(body.get("body").getAsString());
+ thread.setForum(body.get("forumId").getAsString());
+ thread.setAuthor(UUID.fromString(body.get("author").getAsString()));
+ thread.setCreatedAt(System.currentTimeMillis());
+ thread.setLastEditedBy(null);
+ thread.setLastEditedAt(-1L);
+ thread.setLastReplyAt(-1L);
+ thread.setPinned(false);
+ thread.setLocked(false);
+
+ threadService.saveThread(thread);
+
+ return new ResponseEntity<>(thread.toJson(), HttpStatus.OK);
+ }
+
+ @PutMapping(path = "/{id}")
+ public ResponseEntity editThread(@RequestBody JsonObject body, @PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional threadOpt = threadService.getThread(id);
+ if (!threadOpt.isPresent()) {
+ response.add("message", "Thread not found.");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumThread thread = threadOpt.get();
+
+ if (body.has("title"))
+ thread.setTitle(body.get("title").getAsString());
+
+ if (body.has("body"))
+ thread.setBody(body.get("body").getAsString());
+
+ if (body.has("pinned"))
+ thread.setPinned(body.get("pinned").getAsBoolean());
+
+ if (body.has("locked"))
+ thread.setLocked(body.get("locked").getAsBoolean());
+
+ if (body.has("lastEditedBy"))
+ thread.setLastEditedBy(UUID.fromString(body.get("lastEditedBy").getAsString()));
+
+ if (body.has("lastEditedAt"))
+ thread.setLastEditedAt(body.get("lastEditedAt").getAsLong());
+
+ threadService.saveThread(thread);
+ return new ResponseEntity<>(thread.toJson(), HttpStatus.OK);
+ }
+
+ @DeleteMapping(path = "/{id}")
+ public ResponseEntity deleteThread(@PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional threadOpt = threadService.getThread(id);
+ if (!threadOpt.isPresent()) {
+ response.add("message", "Thread not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumThread thread = threadOpt.get();
+ threadService.deleteThread(thread);
+ return new ResponseEntity<>(thread.toJson(), HttpStatus.OK);
+ }
+
+ @DeleteMapping(path = "/{parentId}/{id}")
+ public ResponseEntity deleteReply(@PathVariable(name = "parentId") String parentId,
+ @PathVariable(name = "id") String replyId) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional parentThreadOpt = threadService.getThread(parentId);
+ Optional replyThreadOpt = threadService.getThread(replyId);
+ if (!parentThreadOpt.isPresent() || !replyThreadOpt.isPresent()) {
+ response.add("message", "Thread not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumThread replyThread = replyThreadOpt.get();
+ ForumThread parentThread = parentThreadOpt.get();
+
+ parentThread.getReplies().remove(replyThread);
+ threadService.deleteThread(replyThread);
+ threadService.saveThread(parentThread);
+
+ return new ResponseEntity<>(replyThread.toJson(), HttpStatus.OK);
+ }
+
+
+ @GetMapping(path = "/{id}")
+ public ResponseEntity getThread(@PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional threadOpt = threadService.getThread(id);
+ if (!threadOpt.isPresent()) {
+ response.add("message", "Thread not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ return new ResponseEntity<>(threadOpt.get().toJson(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/forum/{id}")
+ public ResponseEntity getForumThreads(@PathVariable(name = "id") String forumId,
+ @RequestParam(name = "page", defaultValue = "1") int page) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional forumOpt = api.getForumService().getForumModelService()
+ .getByIdOrName(forumId.replace("-", " "));
+ if (!forumOpt.isPresent()) {
+ response.add("message", "Forum not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumModel forum = forumOpt.get();
+
+ List threads = threadService.getForumThreads(forum.getId(), page);
+ JsonArray array = new JsonArray();
+
+ threads.forEach(thread -> array.add(thread.toJson()));
+ return new ResponseEntity<>(array, HttpStatus.OK);
+ }
+
+ @PostMapping(path = "/{parentId}/reply")
+ public ResponseEntity createReply(@RequestBody JsonObject body, @PathVariable(name = "parentId") String parentId) {
+ JsonBuilder response = new JsonBuilder();
+
+ String id = body.get("id").getAsString();
+ Optional threadOpt = threadService.getThread(id);
+ if (threadOpt.isPresent()) {
+ response.add("message", "Thread already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ Optional parentOpt = threadService.getThread(parentId);
+ if (!parentOpt.isPresent()) {
+ response.add("message", "Parent thread not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumThread parent = parentOpt.get();
+ ForumThread thread = new ForumThread();
+
+ thread.setId(id);
+ thread.setParentThreadId(parent.getId());
+ thread.setTitle(body.get("title").getAsString());
+ thread.setBody(body.get("body").getAsString());
+ thread.setForum(body.get("forumId").getAsString());
+ thread.setAuthor(UUID.fromString(body.get("author").getAsString()));
+ thread.setCreatedAt(System.currentTimeMillis());
+ thread.setLastEditedBy(null);
+ thread.setLastEditedAt(-1L);
+ thread.setLastReplyAt(-1L);
+ thread.setPinned(false);
+ thread.setLocked(false);
+
+ parent.setLastReplyAt(System.currentTimeMillis());
+ parent.getReplies().add(thread);
+
+ threadService.saveThread(thread);
+ threadService.saveThread(parent);
+
+ return new ResponseEntity<>(thread.toJson(), HttpStatus.OK);
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/thread/ThreadService.java b/API/src/main/java/org/hcrival/api/forum/thread/ThreadService.java
new file mode 100644
index 0000000..ad50b68
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/thread/ThreadService.java
@@ -0,0 +1,89 @@
+package org.hcrival.api.forum.thread;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.util.exception.DataNotFoundException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+public class ThreadService {
+
+ private final InvictusAPI master;
+
+ @Getter
+ private final LoadingCache cache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public ForumThread load(String id) throws DataNotFoundException {
+ Document document = master.getMongoService().getForumThreads()
+ .find(Filters.eq("id", id)).first();
+ if (document == null)
+ throw new DataNotFoundException();
+
+ return new ForumThread(document);
+ }
+ });
+
+ public Optional getThread(String id) {
+ try {
+ return Optional.ofNullable(cache.get(id));
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public void saveThread(ForumThread thread) {
+ master.getMongoService().getForumThreads().replaceOne(
+ Filters.eq("id", thread.getId()),
+ thread.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(thread.getId(), thread);
+ }
+
+ public void deleteThread(ForumThread thread) {
+ master.getMongoService().getForumThreads().deleteOne(Filters.or(
+ Filters.eq("id", thread.getId()),
+ Filters.eq("parentThreadId", thread.getId())
+ ));
+ cache.asMap().remove(thread.getId());
+ }
+
+ public List getProfileThreads(UUID uuid, int page) {
+ List threads = new ArrayList<>();
+ master.getMongoService().getForumThreads().find(Filters.and(
+ Filters.eq("parentThreadId", null),
+ Filters.eq("author", uuid.toString())))
+ .forEach((Block super Document>) document -> threads.add(new ForumThread(document)));
+ return threads;
+ }
+
+ public List getForumThreads(String forumId, int page) {
+ List threads = new ArrayList<>();
+ master.getMongoService().getForumThreads().find(Filters.and(
+ Filters.eq("parentThreadId", null),
+ Filters.eq("forum", forumId)
+ )).forEach((Block super Document>) document -> threads.add(new ForumThread(document)));
+ return threads;
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/ticket/ForumTicket.java b/API/src/main/java/org/hcrival/api/forum/ticket/ForumTicket.java
new file mode 100644
index 0000000..b6d2f2e
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/ticket/ForumTicket.java
@@ -0,0 +1,77 @@
+package org.hcrival.api.forum.ticket;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.profile.Profile;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class ForumTicket {
+
+ private String id;
+ private String category;
+ private String body;
+ private String status;
+
+ private UUID author;
+ private long createdAt;
+
+ private long lastUpdatedAt;
+ private transient List replies = new ArrayList<>();
+
+ public ForumTicket(Document document) {
+ this.id = document.getString("id");
+ this.category = document.getString("category");
+ this.body = document.getString("body");
+ this.author = UUID.fromString(document.getString("author"));
+ this.createdAt = document.getLong("createdAt");
+ this.lastUpdatedAt = document.getLong("lastUpdatedAt");
+ this.status = document.getString("status");
+
+ document.getList("replies", Document.class)
+ .forEach(replyDocument -> replies.add(new TicketReply(replyDocument)));
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("id", id);
+ document.append("category", category);
+ document.append("body", body);
+ document.append("author", author.toString());
+ document.append("createdAt", createdAt);
+ document.append("lastUpdatedAt", lastUpdatedAt);
+ document.append("status", status);
+
+ List documents = new ArrayList<>();
+ replies.forEach(reply -> documents.add(reply.toBson()));
+ document.append("replies", documents);
+
+ return document;
+ }
+
+ public JsonObject toJson() {
+ JsonObject object = JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+
+ Profile profile = InvictusAPI.getInstance().getProfileService().getProfile(author).orElse(null);
+ if (profile != null) {
+ object.addProperty("authorName", profile.getName());
+ object.addProperty("authorWebColor", profile.getRealCurrentGrant().asRank().getWebColor());
+ }
+
+ JsonArray array = new JsonArray();
+ replies.forEach(reply -> array.add(reply.toJson()));
+ object.add("replies", array);
+
+ return object;
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/ticket/TicketController.java b/API/src/main/java/org/hcrival/api/forum/ticket/TicketController.java
new file mode 100644
index 0000000..2c8ffa7
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/ticket/TicketController.java
@@ -0,0 +1,142 @@
+package org.hcrival.api.forum.ticket;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RestController
+@RequestMapping(path = "/forum/ticket")
+public class TicketController {
+
+ private final InvictusAPI api;
+ private final TicketService ticketService;
+
+ public TicketController(InvictusAPI api) {
+ this.api = api;
+ this.ticketService = api.getForumService().getTicketService();
+ }
+
+ @PostMapping
+ public ResponseEntity createTicket(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+
+ String id = body.get("id").getAsString();
+ Optional ticketOpt = ticketService.getTicket(id);
+ if (ticketOpt.isPresent()) {
+ response.add("message", "Ticket already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ ForumTicket ticket = new ForumTicket();
+ ticket.setId(id);
+ ticket.setCategory(body.get("category").getAsString());
+ ticket.setBody(body.get("body").getAsString());
+ ticket.setStatus(body.get("status").getAsString());
+ ticket.setAuthor(UUID.fromString(body.get("author").getAsString()));
+ ticket.setCreatedAt(System.currentTimeMillis());
+ ticket.setLastUpdatedAt(System.currentTimeMillis());
+
+ ticketService.saveTicket(ticket);
+
+ return new ResponseEntity<>(ticket.toJson(), HttpStatus.OK);
+ }
+
+ @PutMapping(path = "/{id}")
+ public ResponseEntity editTicket(@RequestBody JsonObject body, @PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional ticketOpt = ticketService.getTicket(id);
+ if (!ticketOpt.isPresent()) {
+ response.add("message", "Ticket not found.");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ ForumTicket ticket = ticketOpt.get();
+
+ if (body.has("category"))
+ ticket.setCategory(body.get("title").getAsString());
+
+ if (body.has("body"))
+ ticket.setBody(body.get("body").getAsString());
+
+ if (body.has("status"))
+ ticket.setStatus(body.get("status").getAsString());
+
+ ticket.setLastUpdatedAt(System.currentTimeMillis());
+
+ ticketService.saveTicket(ticket);
+ return new ResponseEntity<>(ticket.toJson(), HttpStatus.OK);
+ }
+
+
+ @GetMapping(path = "/{id}")
+ public ResponseEntity getTicket(@PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional ticketOpt = ticketService.getTicket(id);
+ if (!ticketOpt.isPresent()) {
+ response.add("message", "Ticket not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ return new ResponseEntity<>(ticketOpt.get().toJson(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/player/{uuid}")
+ public ResponseEntity getPlayerTickets(@PathVariable(name = "uuid") UUID uuid,
+ @RequestParam(name = "page", defaultValue = "1") int page) {
+ JsonArray array = new JsonArray();
+
+ List tickets = ticketService.getPlayerTickets(uuid, page);
+ tickets.forEach(ticket -> array.add(ticket.toJson()));
+
+ return new ResponseEntity<>(array, HttpStatus.OK);
+ }
+
+ // makes this /forum/tickets/all, clashing with /forum/ticket/{id} otherwise
+ @GetMapping(path = "/admin")
+ public ResponseEntity getAllTickets(@RequestParam(name = "page", defaultValue = "1") int page) {
+ JsonArray array = new JsonArray();
+
+ List tickets = ticketService.getAllTickets(page);
+ tickets.forEach(ticket -> array.add(ticket.toJson()));
+
+ return new ResponseEntity<>(array, HttpStatus.OK);
+ }
+
+ @PostMapping(path = "/{parentId}/reply")
+ public ResponseEntity createReply(@RequestBody JsonObject body, @PathVariable(name = "parentId") String parentId) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional parentOpt = ticketService.getTicket(parentId);
+ if (!parentOpt.isPresent()) {
+ response.add("message", "Parent ticket not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ ForumTicket parent = parentOpt.get();
+ TicketReply reply = new TicketReply();
+
+ reply.setId(body.get("id").getAsString());
+ reply.setBody(body.get("body").getAsString());
+ reply.setAuthor(UUID.fromString(body.get("author").getAsString()));
+ reply.setCreatedAt(System.currentTimeMillis());
+ reply.setParentTicketId(parent.getId());
+
+ parent.setLastUpdatedAt(System.currentTimeMillis());
+ parent.getReplies().add(reply);
+
+ ticketService.saveTicket(parent);
+ return new ResponseEntity<>(reply.toJson(), HttpStatus.OK);
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/ticket/TicketReply.java b/API/src/main/java/org/hcrival/api/forum/ticket/TicketReply.java
new file mode 100644
index 0000000..43f8569
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/ticket/TicketReply.java
@@ -0,0 +1,53 @@
+package org.hcrival.api.forum.ticket;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.profile.Profile;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class TicketReply {
+
+ private String id;
+ private String body;
+ private UUID author;
+ private long createdAt;
+ private String parentTicketId;
+
+ public TicketReply(Document document) {
+ this.id = document.getString("id");
+ this.body = document.getString("body");
+ this.author = UUID.fromString(document.getString("author"));
+ this.createdAt = document.getLong("createdAt");
+ this.parentTicketId = document.getString("parentTicketId");
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("id", id);
+ document.append("body", body);
+ document.append("author", author.toString());
+ document.append("createdAt", createdAt);
+ document.append("parentTicketId", parentTicketId);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ JsonObject object = JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+
+ Profile profile = InvictusAPI.getInstance().getProfileService().getProfile(author).orElse(null);
+ if (profile != null) {
+ object.addProperty("authorName", profile.getName());
+ object.addProperty("authorWebColor", profile.getRealCurrentGrant().asRank().getWebColor());
+ }
+
+ return object;
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/ticket/TicketService.java b/API/src/main/java/org/hcrival/api/forum/ticket/TicketService.java
new file mode 100644
index 0000000..25611c3
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/ticket/TicketService.java
@@ -0,0 +1,80 @@
+package org.hcrival.api.forum.ticket;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.util.exception.DataNotFoundException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+public class TicketService {
+
+ private final InvictusAPI master;
+
+ @Getter
+ private final LoadingCache cache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public ForumTicket load(String id) throws DataNotFoundException {
+ Document document = master.getMongoService().getForumTickets()
+ .find(Filters.eq("id", id)).first();
+ if (document == null)
+ throw new DataNotFoundException();
+
+ return new ForumTicket(document);
+ }
+ });
+
+ public Optional getTicket(String id) {
+ try {
+ return Optional.ofNullable(cache.get(id));
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public void saveTicket(ForumTicket ticket) {
+ master.getMongoService().getForumTickets().replaceOne(
+ Filters.eq("id", ticket.getId()),
+ ticket.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(ticket.getId(), ticket);
+ }
+
+ public void deleteTicket(ForumTicket ticket) {
+ master.getMongoService().getForumTickets().deleteOne(Filters.eq("id", ticket.getId()));
+ cache.asMap().remove(ticket.getId());
+ }
+
+ public List getPlayerTickets(UUID uuid, int page) {
+ List tickets = new ArrayList<>();
+ master.getMongoService().getForumTickets().find(Filters.eq("author", uuid.toString()))
+ .forEach((Block super Document>) document -> tickets.add(new ForumTicket(document)));
+ return tickets;
+ }
+
+ public List getAllTickets(int page) {
+ List tickets = new ArrayList<>();
+ master.getMongoService().getForumTickets().find()
+ .forEach((Block super Document>) document -> tickets.add(new ForumTicket(document)));
+ return tickets;
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/trophy/TrophyController.java b/API/src/main/java/org/hcrival/api/forum/trophy/TrophyController.java
new file mode 100644
index 0000000..b7641df
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/trophy/TrophyController.java
@@ -0,0 +1,85 @@
+package org.hcrival.api.forum.trophy;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Optional;
+
+@RestController
+@RequestMapping(path = "/forum/trophy")
+public class TrophyController {
+
+ private final InvictusAPI api;
+ private final TrophyService trophyService;
+
+ public TrophyController(InvictusAPI api) {
+ this.api = api;
+ this.trophyService = api.getForumService().getTrophyService();
+ }
+
+ @GetMapping
+ public ResponseEntity listAll() {
+ JsonArray response = new JsonArray();
+ trophyService.getTrophies().forEach(trophy -> response.add(trophy.toJson()));
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ }
+
+
+ @DeleteMapping(path = "/{id}")
+ public ResponseEntity deleteTrophy(@PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional trophyOpt = trophyService.getById(id);
+ if (!trophyOpt.isPresent()) {
+ response.add("message", "Trophy not found.");
+ return new ResponseEntity(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ TrophyModel trophy = trophyOpt.get();
+ trophyService.deleteTrophy(trophy);
+
+ return new ResponseEntity(trophy.toJson(), HttpStatus.OK);
+ }
+
+
+ @PostMapping()
+ public ResponseEntity createTrophy(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+
+ String id = body.get("id").getAsString();
+ Optional trophyOpt = trophyService.getById(id);
+ if (trophyOpt.isPresent()) {
+ response.add("message", "Trophy already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ TrophyModel trophyModel = new TrophyModel();
+ trophyModel.setId(id);
+ trophyModel.setName(body.get("name").getAsString());
+
+ trophyService.saveTrophy(trophyModel);
+ return new ResponseEntity<>(trophyModel.toJson(), HttpStatus.CREATED);
+ }
+
+ @GetMapping(path = "/{id}")
+ public ResponseEntity getTrophy(@PathVariable(name = "id") String id) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional trophyOpt = trophyService.getById(id);
+
+ if (!trophyOpt.isPresent()) {
+ response.add("message", "Trophy not found.");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ return new ResponseEntity<>(trophyOpt.get().toJson(), HttpStatus.OK);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/trophy/TrophyModel.java b/API/src/main/java/org/hcrival/api/forum/trophy/TrophyModel.java
new file mode 100644
index 0000000..5af8d5a
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/trophy/TrophyModel.java
@@ -0,0 +1,34 @@
+package org.hcrival.api.forum.trophy;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+@Data
+@NoArgsConstructor
+public class TrophyModel {
+
+ private String id;
+ private String name;
+
+ public TrophyModel(Document document) {
+ this.id = document.getString("id");
+ this.name = document.getString("name");
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+
+ document.append("id", id);
+ document.append("name", name);
+
+ return document;
+ }
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/forum/trophy/TrophyService.java b/API/src/main/java/org/hcrival/api/forum/trophy/TrophyService.java
new file mode 100644
index 0000000..f89fec1
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/forum/trophy/TrophyService.java
@@ -0,0 +1,54 @@
+package org.hcrival.api.forum.trophy;
+
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@RequiredArgsConstructor
+public class TrophyService {
+
+ private final InvictusAPI master;
+
+ private final Map cache = new ConcurrentHashMap<>();
+
+ public void loadTrophies() {
+ master.getMongoService().getForumTrophies().find().forEach((Block super Document>) document -> {
+ TrophyModel trophy = new TrophyModel(document);
+ cache.put(trophy.getId(), trophy);
+ });
+ }
+
+
+ public Optional getById(String id) {
+ return Optional.ofNullable(cache.get(id));
+ }
+
+ public List getTrophies() {
+ return new ArrayList<>(cache.values());
+ }
+
+ public void saveTrophy(TrophyModel trophy) {
+ master.getMongoService().getForumTrophies().replaceOne(
+ Filters.eq("id", trophy.getId()),
+ trophy.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(trophy.getId(), trophy);
+ }
+
+ public void deleteTrophy(TrophyModel trophy) {
+ master.getMongoService().getForumTrophies().deleteOne(Filters.eq("id", trophy.getId()));
+ cache.remove(trophy.getId());
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/mongo/MongoService.java b/API/src/main/java/org/hcrival/api/mongo/MongoService.java
new file mode 100644
index 0000000..21fac2a
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/mongo/MongoService.java
@@ -0,0 +1,90 @@
+package org.hcrival.api.mongo;
+
+import com.mongodb.MongoClient;
+import com.mongodb.MongoCredential;
+import com.mongodb.ServerAddress;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.ReplaceOptions;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.util.configuration.defaults.MongoConfig;
+
+import java.util.Collections;
+
+@Getter
+@RequiredArgsConstructor
+public class MongoService {
+
+ public static final ReplaceOptions REPLACE_OPTIONS = new ReplaceOptions().upsert(true);
+
+ private final InvictusAPI master;
+ private MongoClient client;
+
+ // Invictus
+
+ private MongoDatabase invictusDatabase;
+ private MongoDatabase forumsDatabase;
+
+ private MongoCollection ranks;
+ private MongoCollection tags;
+ private MongoCollection punishments;
+ private MongoCollection profiles;
+ private MongoCollection grants;
+ private MongoCollection notes;
+ private MongoCollection disguiseData;
+ private MongoCollection discordData;
+ private MongoCollection banphrases;
+
+ private MongoCollection forumAccounts;
+ private MongoCollection forumCategories;
+ private MongoCollection forumForums;
+ private MongoCollection forumThreads;
+ private MongoCollection forumTickets;
+ private MongoCollection forumTrophies;
+
+ public boolean connect() {
+ MongoConfig config = master.getMainConfig().getMongoConfig();
+ if (config.isAuthEnabled()) {
+ MongoCredential credential = MongoCredential.createCredential(
+ config.getAuthUsername(),
+ config.getAuthDatabase(),
+ config.getAuthPassword().toCharArray()
+ );
+
+ client = new MongoClient(
+ new ServerAddress(config.getHost(), config.getPort()),
+ Collections.singletonList(credential)
+ );
+ } else client = new MongoClient(config.getHost(), config.getPort());
+
+ try {
+ invictusDatabase = client.getDatabase("invictus");
+ ranks = invictusDatabase.getCollection("ranks");
+ tags = invictusDatabase.getCollection("tags");
+ punishments = invictusDatabase.getCollection("punishments");
+ profiles = invictusDatabase.getCollection("profiles");
+ grants = invictusDatabase.getCollection("grants");
+ notes = invictusDatabase.getCollection("notes");
+ disguiseData = invictusDatabase.getCollection("disguiseData");
+ discordData = invictusDatabase.getCollection("discordData");
+ banphrases = invictusDatabase.getCollection("banphrases");
+
+ forumsDatabase = client.getDatabase("forums");
+ forumAccounts = forumsDatabase.getCollection("accounts");
+ forumCategories = forumsDatabase.getCollection("categories");
+ forumForums = forumsDatabase.getCollection("forums");
+ forumThreads = forumsDatabase.getCollection("threads");
+ forumTickets = forumsDatabase.getCollection("tickets");
+ forumTrophies = forumsDatabase.getCollection("trophies");
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/Profile.java b/API/src/main/java/org/hcrival/api/profile/Profile.java
new file mode 100644
index 0000000..2fa381b
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/Profile.java
@@ -0,0 +1,148 @@
+package org.hcrival.api.profile;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.profile.grant.Grant;
+import org.hcrival.api.profile.grant.GrantService;
+import org.hcrival.api.punishment.Punishment;
+import org.hcrival.api.punishment.PunishmentService;
+import org.hcrival.api.rank.RankService;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Data
+@NoArgsConstructor
+public class Profile {
+
+ private static final GrantService GRANT_SERVICE = InvictusAPI.getInstance().getGrantService();
+ private static final PunishmentService PUNISHMENT_SERVICE = InvictusAPI.getInstance().getPunishmentService();
+ private static final RankService RANK_SERVICE = InvictusAPI.getInstance().getRankService();
+
+ private UUID uuid;
+ private String name = "N/A";
+ private String lastIp = "N/A";
+ private List knownIps = new ArrayList<>();
+ private ProfileOptions options = new ProfileOptions();
+ private List permissions = new ArrayList<>();
+ private long firstLogin = -1;
+ private long lastSeen = -1;
+ private long joinTime = -1;
+ private long playTime = -1;
+ private String lastServer = null;
+ private String activeTag = null;
+
+ public Profile(Document document) {
+ this.uuid = UUID.fromString(document.getString("uuid"));
+ this.name = document.getString("name");
+ this.lastIp = document.getString("lastIp");
+ this.knownIps = document.getList("knownIps", String.class);
+ this.options = new ProfileOptions(document.get("options", Document.class));
+ this.permissions = document.getList("permissions", String.class);
+ this.firstLogin = document.get("firstLogin", Number.class).longValue();
+ this.lastSeen = document.get("lastSeen", Number.class).longValue();
+ this.joinTime = document.get("joinTime", Number.class).longValue();
+ this.playTime = document.get("playTime", Number.class).longValue();
+ this.lastServer = document.getString("lastServer");
+ this.activeTag = document.getString("lastTag");
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("uuid", uuid.toString());
+ document.append("name", name);
+ document.append("lastIp", lastIp);
+ document.append("knownIps", knownIps);
+ document.append("options", options.toBson());
+ document.append("permissions", permissions);
+ document.append("firstLogin", firstLogin);
+ document.append("lastSeen", lastSeen);
+ document.append("joinTime", joinTime);
+ document.append("playTime", playTime);
+ document.append("lastServer", lastServer);
+ document.append("activeTag", activeTag);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ JsonObject object = JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+
+ JsonArray grants = new JsonArray();
+ getActiveGrants().forEach(grant -> grants.add(grant.toJson()));
+ object.add("activeGrants", grants);
+ return object;
+ }
+
+ public Grant getRealCurrentGrant() {
+ Grant grant = null;
+
+ for (Grant current : this.getActiveGrants()) {
+ if (grant == null) {
+ grant = current;
+ continue;
+ }
+
+ if (current.asRank().getWeight() > grant.asRank().getWeight())
+ grant = current;
+ }
+
+ return grant;
+ }
+
+ public List getActiveGrants() {
+ List activeGrants = GRANT_SERVICE.getGrantsOf(uuid).stream()
+ .filter(grant -> !grant.isRemoved() && grant.isActive() && grant.asRank() != null)
+ .collect(Collectors.toList());
+
+ if (activeGrants.isEmpty()) {
+ Grant grant = new Grant();
+ grant.setId(UUID.randomUUID());
+ grant.setUuid(uuid);
+ grant.setRank(RANK_SERVICE.getDefaultRank().getUuid());
+ grant.setGrantedAt(System.currentTimeMillis());
+ grant.setGrantedReason("Default Grant");
+ grant.setGrantedBy("Console");
+ grant.setScopes("GLOBAL");
+ grant.setDuration(-1);
+ grant.setEnd(-1);
+ GRANT_SERVICE.saveGrant(grant);
+ return getActiveGrants();
+ }
+
+ return activeGrants;
+ }
+
+ public List getActiveGrantsOn(String scope) {
+ return getActiveGrants().stream()
+ .filter(grant -> grant.isActiveOnScope(scope))
+ .collect(Collectors.toList());
+ }
+
+ public boolean hasGrantOf(String rank) {
+ return getActiveGrants().stream()
+ .anyMatch(grant -> grant.asRank().getName().equalsIgnoreCase(rank));
+ }
+
+ public boolean hasGrantOfOn(String rank, String scope) {
+ return getActiveGrantsOn(scope).stream()
+ .anyMatch(grant -> grant.asRank().getName().equalsIgnoreCase(rank));
+ }
+
+ public Optional getActivePunishment(String type) {
+ return PUNISHMENT_SERVICE.getPunishmentsOf(uuid).stream()
+ .filter(punishment -> punishment.isActive()
+ && !punishment.isRemoved()
+ && punishment.getPunishmentType().equals(type))
+ .findFirst();
+ }
+
+ public String getDisplayName() {
+ return getRealCurrentGrant().asRank().getColor() + name;
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/ProfileController.java b/API/src/main/java/org/hcrival/api/profile/ProfileController.java
new file mode 100644
index 0000000..2d004ba
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/ProfileController.java
@@ -0,0 +1,268 @@
+package org.hcrival.api.profile;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.discord.DiscordData;
+import org.hcrival.api.forum.account.ForumAccount;
+import org.hcrival.api.punishment.Punishment;
+import org.hcrival.api.rank.Rank;
+import org.hcrival.api.util.JsonBuilder;
+import org.hcrival.api.util.PermissionUtil;
+import org.hcrival.api.util.UUIDCache;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Optional;
+import java.util.UUID;
+
+@RestController
+@RequestMapping(path = "/profile")
+public class ProfileController {
+
+ private final InvictusAPI api;
+ private final ProfileService profileService;
+
+ public ProfileController(InvictusAPI api) {
+ this.api = api;
+ this.profileService = api.getProfileService();
+ }
+
+ @PostMapping(path = "/{uuid}/login")
+ public ResponseEntity profileLogin(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ String name = body.get("name").getAsString();
+ String ip = body.get("ip").getAsString();
+ long timeStamp = body.get("timeStamp").getAsLong();
+ String server = body.get("server").getAsString();
+
+ Optional profileOpt = profileService.getProfile(uuid);
+ Profile profile;
+ if (profileOpt.isPresent())
+ profile = profileOpt.get();
+ else {
+ profile = new Profile();
+ profile.setUuid(uuid);
+ profile.setName(name);
+ profile.setFirstLogin(timeStamp);
+// api.getStatsTracker().addRegistration();
+ }
+
+ if (profile.getLastIp() == null)
+ profile.setLastIp(ip);
+
+ Optional activePunishment = profile.getActivePunishment("BLACKLIST");
+ if (!activePunishment.isPresent())
+ activePunishment = profile.getActivePunishment("BAN");
+
+ if (!activePunishment.isPresent()) {
+ Optional evasionPunishment = profileService.getAlts(profile).stream()
+ .filter(alt -> alt.getLastIp() != null
+ && profile.getLastIp() != null
+ && alt.getLastIp().equals(profile.getLastIp())
+ && alt.getActivePunishment("BLACKLIST").isPresent()).findFirst()
+ .flatMap(alt -> alt.getActivePunishment("BLACKLIST"));
+
+ if (!evasionPunishment.isPresent())
+ evasionPunishment = profileService.getAlts(profile).stream()
+ .filter(alt -> alt.getLastIp() != null
+ && profile.getLastIp() != null
+ && alt.getLastIp().equals(profile.getLastIp())
+ && alt.getActivePunishment("BAN").isPresent()).findFirst()
+ .flatMap(alt -> alt.getActivePunishment("BAN"));
+
+ if (evasionPunishment.isPresent()
+ && !profile.hasGrantOfOn("evasion-bypass", body.get("grantScope").getAsString())) {
+ Punishment punishment = new Punishment();
+ punishment.setId(UUID.randomUUID());
+ punishment.setUuid(profile.getUuid());
+ punishment.setPunishedBy("Console");
+ punishment.setPunishmentType(evasionPunishment.get().getPunishmentType());
+ punishment.setPunishedServerType("Server");
+ punishment.setPunishedServer(server);
+ punishment.setPunishedAt(System.currentTimeMillis());
+
+ Profile alt = profileService.getProfile(evasionPunishment.get().getUuid()).orElse(null);
+ punishment.setPunishedReason(String.format("%s Evading (%s)",
+ evasionPunishment.get().getPunishmentType().equals("BLACKLIST") ? "Blacklist" : "Ban",
+ alt != null ? alt.getName() : "Unknown Player"
+ ));
+
+ long duration = evasionPunishment.get().getEnd() == -1 ? -1
+ : evasionPunishment.get().getEnd() - System.currentTimeMillis();
+ punishment.setDuration(duration);
+ punishment.setEnd(evasionPunishment.get().getEnd());
+
+ api.getPunishmentService().savePunishment(punishment);
+
+ activePunishment = Optional.of(punishment);
+ response.add("evasionPunishment", true);
+ }
+
+ }
+
+ activePunishment.ifPresent(punishment -> {
+ response.add("activePunishment", punishment.toJson());
+ if (!profile.getLastIp().equals(ip))
+ profile.setLastIp(ip);
+ if (!profile.getKnownIps().contains(ip))
+ profile.getKnownIps().add(ip);
+ });
+
+ Optional discordData = api.getDiscordService().getByUuid(uuid);
+ response.add("boosted", discordData.isPresent() && discordData.get().isBoosted());
+
+ profileService.saveProfile(profile);
+
+ response.add("isOnVPN", false/*antiVPNService.isVPN(ip)*/);
+
+ response.add("profile", profile.toJson());
+
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @PostMapping(path = "/{uuid}/join")
+ public ResponseEntity profileJoin(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ Optional profileOpt = profileService.getProfile(uuid);
+
+ if (!profileOpt.isPresent()) {
+ response.add("message", "Profile not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Profile profile = profileOpt.get();
+ String ip = body.get("ip").getAsString();
+ boolean staff = body.get("staff").getAsBoolean();
+ String server = body.get("server").getAsString();
+ long timeStamp = body.get("timeStamp").getAsLong();
+
+ response.add("lastServer", profile.getLastServer());
+
+ if (!profile.getLastIp().equals(ip)) {
+ if (staff)
+ response.add("requiresTotp", true);
+ else profile.setLastIp(ip);
+ }
+
+ if (!profile.getKnownIps().contains(ip))
+ profile.getKnownIps().add(ip);
+
+ profile.setLastServer(server);
+ if (profile.getFirstLogin() == -1)
+ profile.setFirstLogin(timeStamp);
+ profile.setLastSeen(timeStamp);
+ profile.setJoinTime(timeStamp);
+ profileService.saveProfile(profile);
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/{uuid}")
+ public ResponseEntity getProfile(@PathVariable(name = "uuid") UUID uuid,
+ @RequestParam(name = "webResolved", defaultValue = "false") boolean web,
+ @RequestParam(name = "includePermissions", defaultValue = "false") boolean perms) {
+ JsonBuilder response = new JsonBuilder();
+ Optional profileOpt = profileService.getProfile(uuid);
+
+ if (!profileOpt.isPresent()) {
+ response.add("message", "Profile not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Profile profileObj = profileOpt.get();
+ JsonObject profile = profileObj.toJson();
+
+ if (web) {
+ JsonArray activeGrants = new JsonArray();
+ api.getGrantService().getGrantsOf(profileObj.getUuid()).forEach(grant -> activeGrants.add(grant.toJson()));
+
+ for (JsonElement element : activeGrants) {
+ JsonObject grant = element.getAsJsonObject();
+ String grantedBy = grant.get("grantedBy").getAsString();
+ grant.addProperty("resolvedGrantedBy", UUIDCache.isUuid(grantedBy)
+ ? UUIDCache.getName(UUID.fromString(grantedBy))
+ : grantedBy);
+
+ if (grant.get("removedBy") != null) {
+ String removedBy = grant.get("removedBy").getAsString();
+ grant.addProperty("resolvedRemovedBy", UUIDCache.isUuid(removedBy)
+ ? UUIDCache.getName(UUID.fromString(removedBy))
+ : removedBy);
+ }
+
+ Optional rankOpt = api.getRankService().getRank(UUID.fromString(grant.get("rank").getAsString()));
+ grant.addProperty("resolvedRank", rankOpt.isPresent() ? rankOpt.get().getName() : "???");
+ grant.addProperty("webColor", rankOpt.isPresent() ? rankOpt.get().getWebColor() : "#FFFFFF");
+ }
+
+ Rank rank = profileObj.getRealCurrentGrant().asRank();
+ JsonObject rankObject = rank.toJson();
+ rankObject.addProperty("webColor", rank.getWebColor());
+ profile.add("rank", rankObject);
+ profile.add("activeGrants", activeGrants);
+
+ JsonObject settings = new JsonObject();
+
+ Optional accountOpt = api.getForumService().getAccountService().getAccount(profileObj.getUuid());
+ if (accountOpt.isPresent()) {
+ ForumAccount account = accountOpt.get();
+ account.getSettings().forEach(settings::addProperty);
+ }
+
+ profile.add("webSettings", settings);
+ }
+
+ if (perms) {
+ JsonObject permissions = PermissionUtil.getEffectivePermissions(profileObj);
+ profile.add("effectivePermissions", permissions);
+ profile.addProperty("isOnOplist", api.getMainConfig().getOpList().contains(profileObj.getUuid()));
+ }
+
+ return new ResponseEntity<>(profile, HttpStatus.OK);
+ }
+
+ @PostMapping
+ public ResponseEntity createProfile(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+ Profile profile = JsonConfigurationService.gson.fromJson(body, Profile.class);
+ if (profileService.getProfile(profile.getUuid()).isPresent()) {
+ response.add("message", "Profile already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ profileService.saveProfile(profile);
+// api.getStatsTracker().addRegistration();
+ return new ResponseEntity<>(profile.toJson(), HttpStatus.CREATED);
+ }
+
+ @PutMapping
+ public ResponseEntity updateProfile(@RequestBody JsonObject body) {
+ JsonBuilder response = new JsonBuilder();
+ Profile profile = JsonConfigurationService.gson.fromJson(body, Profile.class);
+ if (!profileService.getProfile(profile.getUuid()).isPresent()) {
+ response.add("message", "Profile not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ profileService.saveProfile(profile);
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/{uuid}/alts")
+ public ResponseEntity getAlts(@PathVariable(name = "uuid") UUID uuid) {
+ Optional profileOpt = profileService.getProfile(uuid);
+
+ if (!profileOpt.isPresent())
+ return new ResponseEntity<>(new JsonArray(), HttpStatus.OK);
+
+ Profile profile = profileOpt.get();
+ JsonArray alts = new JsonArray();
+ profileService.getAlts(profile).forEach(alt -> alts.add(alt.toJson()));
+ return new ResponseEntity<>(alts, HttpStatus.OK);
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/ProfileOptions.java b/API/src/main/java/org/hcrival/api/profile/ProfileOptions.java
new file mode 100644
index 0000000..ace8b01
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/ProfileOptions.java
@@ -0,0 +1,47 @@
+package org.hcrival.api.profile;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Data
+@NoArgsConstructor
+public class ProfileOptions {
+
+ private List socialSpy = new ArrayList<>();
+ private List ignoring = new ArrayList<>();
+ private Map customOptions = new HashMap<>();
+
+ public ProfileOptions(Document document) {
+ this.socialSpy = document.getList("socialSpy", String.class);
+ this.ignoring = document.getList("ignoring", String.class).stream()
+ .map(UUID::fromString)
+ .collect(Collectors.toList());
+ document.get("customOptions", Document.class)
+ .forEach((key, value) -> customOptions.put(key, String.valueOf(value)));
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("socialSpy", socialSpy);
+ document.append("ignoring", ignoring.stream()
+ .map(UUID::toString)
+ .collect(Collectors.toList()));
+
+ Document customOptionsDocument = new Document();
+ customOptions.forEach(customOptionsDocument::append);
+
+ document.append("customOptions", customOptionsDocument);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/ProfileService.java b/API/src/main/java/org/hcrival/api/profile/ProfileService.java
new file mode 100644
index 0000000..5df34b3
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/ProfileService.java
@@ -0,0 +1,91 @@
+package org.hcrival.api.profile;
+
+import com.google.common.cache.*;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.util.exception.DataNotFoundException;
+
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+public class ProfileService {
+
+ private final InvictusAPI api;
+
+ @Getter
+ private final LoadingCache cache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public Profile load(UUID uuid) throws DataNotFoundException {
+ Document document = api.getMongoService().getProfiles()
+ .find(Filters.eq("uuid", uuid.toString())).first();
+ if (document == null)
+ throw new DataNotFoundException();
+
+ Profile profile = new Profile(document);
+// Optional playerOpt = api.getPlayerService().getPlayer(profile.getUuid());
+// if (playerOpt.isPresent()) {
+// Player player = playerOpt.get();
+// player.setHandle(profile);
+// player.reloadPermissions();
+// }
+
+ return profile;
+ }
+ });
+
+ public Optional getProfile(UUID uuid) {
+ try {
+ return Optional.ofNullable(cache.get(uuid));
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public void saveProfile(Profile profile) {
+ api.getMongoService().getProfiles().replaceOne(
+ Filters.eq("uuid", profile.getUuid().toString()),
+ profile.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+// Optional playerOpt = api.getPlayerService().getPlayer(profile.getUuid());
+// if (playerOpt.isPresent()) {
+// Player player = playerOpt.get();
+// player.setHandle(profile);
+// player.reloadPermissions();
+// }
+
+ cache.put(profile.getUuid(), profile);
+ }
+
+ public List getAlts(Profile profile) {
+ List toReturn = new ArrayList<>();
+ Set uuidSet = new HashSet<>();
+ for (Document document : api.getMongoService().getProfiles()
+ .find(Filters.in("knownIps", profile.getKnownIps()))) {
+ UUID uuid = UUID.fromString(document.getString("uuid"));
+ if (profile.getUuid().equals(uuid) || uuidSet.contains(uuid))
+ continue;
+
+ Profile alt = new Profile(document);
+ cache.put(uuid, alt);
+ if (!uuidSet.contains(alt.getUuid())) {
+ uuidSet.add(alt.getUuid());
+ toReturn.add(alt);
+ }
+ }
+
+ return toReturn;
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/grant/Grant.java b/API/src/main/java/org/hcrival/api/profile/grant/Grant.java
new file mode 100644
index 0000000..bb4fdd1
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/grant/Grant.java
@@ -0,0 +1,116 @@
+package org.hcrival.api.profile.grant;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.rank.Rank;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.*;
+
+@Data
+@NoArgsConstructor
+public class Grant {
+
+ public static Comparator COMPARATOR = Comparator.comparingInt(grant -> grant.asRank().getWeight());
+
+ private UUID id;
+ private UUID uuid;
+ private UUID rank;
+ private String grantedBy;
+ private long grantedAt;
+ private String grantedReason;
+ private String removedBy = "N/A";
+ private long removedAt = -1;
+ private String removedReason = "N/A";
+ private String scopes;
+ private long duration;
+ private long end;
+ private boolean removed = false;
+
+ public Grant(Document document) {
+ this.id = UUID.fromString(document.getString("id"));
+ this.uuid = UUID.fromString(document.getString("uuid"));
+ this.rank = UUID.fromString(document.getString("rank"));
+ this.grantedBy = document.getString("grantedBy");
+ this.grantedAt = document.get("grantedAt", Number.class).longValue();
+ this.grantedReason = document.getString("grantedReason");
+ this.removedBy = document.getString("removedBy");
+ this.removedAt = document.get("removedAt", Number.class).longValue();
+ this.removedReason = document.getString("removedReason");
+ this.scopes = document.getString("scopes");
+ this.duration = document.get("duration", Number.class).longValue();
+ this.end = document.get("end", Number.class).longValue();
+ this.removed = document.getBoolean("removed");
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("id", id.toString());
+ document.append("uuid", uuid.toString());
+ document.append("rank", rank.toString());
+ document.append("grantedBy", grantedBy);
+ document.append("grantedAt", grantedAt);
+ document.append("grantedReason", grantedReason);
+ document.append("removedBy", removedBy);
+ document.append("removedAt", removedAt);
+ document.append("removedReason", removedReason);
+ document.append("scopes", scopes);
+ document.append("duration", duration);
+ document.append("end", end);
+ document.append("removed", removed);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+ public Rank asRank() {
+ return InvictusAPI.getInstance().getRankService().getRank(rank).orElse(null);
+ }
+
+ public boolean isActive() {
+ if (end == -1) {
+ return true;
+ }
+ return end >= System.currentTimeMillis();
+ }
+
+ public List getScopeList() {
+ return Arrays.asList(scopes.split(","));
+ }
+
+ public boolean isActiveOnScope(String scope) {
+ if (scope.equalsIgnoreCase("GLOBAL"))
+ return true;
+
+ List scopeList = getScopeList();
+ if (scopeList.contains("GLOBAL"))
+ return true;
+
+// ServerGroup serverGroup = Master.getInstance().getGroupService().getServerGroup(scope);
+// if (serverGroup != null) {
+// validScopes.addAll(serverGroup.getConfig().getAcceptingScopes());
+// validScopes.add(serverGroup.getName().toLowerCase());
+// }
+//
+// ProxyGroup proxyGroup = Master.getInstance().getGroupService().getProxyGroup(scope);
+// if (proxyGroup != null)
+// validScopes.add(proxyGroup.getName().toLowerCase());
+//
+// StaticServer staticServer = Master.getInstance().getGroupService().getStaticServer(scope);
+// if (staticServer != null) {
+// validScopes.addAll(staticServer.getAcceptingScopes());
+// validScopes.add(staticServer.getName().toLowerCase());
+// }
+
+ if (scopeList.contains(scope.toLowerCase()))
+ return true;
+
+ return false;
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/grant/GrantController.java b/API/src/main/java/org/hcrival/api/profile/grant/GrantController.java
new file mode 100644
index 0000000..73f9767
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/grant/GrantController.java
@@ -0,0 +1,180 @@
+package org.hcrival.api.profile.grant;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.profile.grant.rpacket.GrantAddPacket;
+import org.hcrival.api.profile.grant.rpacket.GrantRemovePacket;
+import org.hcrival.api.profile.rpacket.ProfileUpdatePacket;
+import org.hcrival.api.rank.Rank;
+import org.hcrival.api.rank.RankService;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RestController
+@RequestMapping(path = "/profile/{uuid}/grants")
+public class GrantController {
+
+ private final InvictusAPI api;
+ private final GrantService grantService;
+ private final RankService rankService;
+
+ public GrantController(InvictusAPI api) {
+ this.api = api;
+ this.grantService = api.getGrantService();
+ this.rankService = api.getRankService();
+ }
+
+ @PostMapping
+ public ResponseEntity addGrant(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+
+ UUID id = body.has("id")
+ ? UUID.fromString(body.get("id").getAsString())
+ : UUID.randomUUID();
+
+ if (grantService.getGrant(id).isPresent()) {
+ response.add("message", "Grant already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ Grant grant = new Grant();
+ grant.setId(id);
+ grant.setUuid(uuid);
+ grant.setRank(UUID.fromString(body.get("rank").getAsString()));
+
+ if (!rankService.getRank(grant.getRank()).isPresent()) {
+ response.add("message", "Rank not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ grant.setGrantedBy(body.get("grantedBy").getAsString());
+ grant.setGrantedAt(body.has("grantedAt")
+ ? body.get("grantedAt").getAsLong()
+ : System.currentTimeMillis());
+ grant.setGrantedReason(body.get("grantedReason").getAsString());
+ grant.setRemovedBy("N/A");
+ grant.setRemovedAt(-1);
+ grant.setRemovedReason("N/A");
+ grant.setScopes(body.get("scopes").getAsString());
+ grant.setDuration(body.get("duration").getAsLong());
+ grant.setEnd(body.has("end")
+ ? body.get("end").getAsLong()
+ : (grant.getDuration() == -1
+ ? -1 : grant.getGrantedAt() + grant.getDuration()));
+ grant.setRemoved(false);
+
+ grantService.saveGrant(grant);
+ api.getRedisService().publish(new GrantAddPacket(grant.getUuid(), grant.getRank(), grant.getDuration()));
+ api.getRedisService().publish(new ProfileUpdatePacket(grant.getUuid()));
+ return new ResponseEntity<>(grant.toJson(), HttpStatus.CREATED);
+ }
+
+ @GetMapping
+ public ResponseEntity getGrantsOf(@PathVariable(name = "uuid") UUID uuid) {
+ JsonArray grants = new JsonArray();
+ grantService.getGrantsOf(uuid).forEach(grant -> grants.add(grant.toJson()));
+ return new ResponseEntity<>(grants, HttpStatus.OK);
+ }
+
+ @PutMapping(path = "/{id}")
+ public ResponseEntity updateGrant(@RequestBody JsonObject body,
+ @PathVariable(name = "uuid") UUID uuid,
+ @PathVariable(name = "id") UUID id) {
+ JsonBuilder response = new JsonBuilder();
+ Optional grantOpt = grantService.getGrant(id);
+
+ if (!grantOpt.isPresent()) {
+ response.add("message", "Grant not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Grant grant = grantOpt.get();
+
+ if (body.has("grantedReason"))
+ grant.setGrantedReason(body.get("grantedReason").getAsString());
+
+ if (body.has("duration")) {
+ grant.setDuration(body.get("duration").getAsLong());
+ grant.setEnd(grant.getDuration() == -1 ? -1
+ : grant.getGrantedAt() + grant.getDuration());
+ }
+
+ if (body.has("removedReason"))
+ grant.setRemovedReason(body.get("removedReason").getAsString());
+
+ boolean didRemove = false;
+ if (body.has("removed") && body.get("removed").getAsBoolean()) {
+ grant.setRemovedAt(body.has("removedAt")
+ ? body.get("removedAt").getAsLong()
+ : System.currentTimeMillis());
+ grant.setRemovedBy(body.get("removedBy").getAsString());
+ grant.setRemoved(true);
+ didRemove = true;
+ }
+
+ grantService.saveGrant(grant);
+
+ if (didRemove)
+ api.getRedisService().publish(new GrantRemovePacket(grant.getUuid(), grant.getRank()));
+
+ api.getRedisService().publish(new ProfileUpdatePacket(grant.getUuid()));
+ return new ResponseEntity<>(grant.toJson(), HttpStatus.OK);
+ }
+
+ @PostMapping(path = "/clear")
+ public ResponseEntity clearGrants(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+
+ AtomicInteger removed = new AtomicInteger();
+ grantService.getGrantsOf(uuid).stream()
+ .filter(grant -> grant.isActive()
+ && !grant.isRemoved()
+ && !grant.asRank().isDefaultRank())
+ .forEach(grant -> {
+ grant.setRemoved(true);
+ grant.setRemovedReason(body.get("removedReason").getAsString());
+ grant.setRemovedAt(body.has("removedAt")
+ ? body.get("removedAt").getAsLong()
+ : System.currentTimeMillis());
+ grant.setRemovedBy(body.get("removedBy").getAsString());
+ grantService.saveGrant(grant);
+ removed.getAndIncrement();
+ });
+
+ response.add("removed", removed.get());
+ return new ResponseEntity<>(response.build(), HttpStatus.OK);
+ }
+
+ @GetMapping(path = "/{id}")
+ public ResponseEntity getSingleGrant(@PathVariable(name = "id") UUID id,
+ @RequestParam(name = "webResolved", defaultValue = "false") boolean web) {
+ JsonBuilder response = new JsonBuilder();
+
+ Optional grantOpt = grantService.getGrant(id);
+ if (!grantOpt.isPresent()) {
+ response.add("message", "Grant not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Grant grant = grantOpt.get();
+ JsonObject object = grant.toJson();
+
+ if (web) {
+ Rank rank = grant.asRank();
+ if (rank != null)
+ object.add("resolvedRank", rank.toJson());
+ }
+
+ return new ResponseEntity<>(object, HttpStatus.OK);
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/grant/GrantService.java b/API/src/main/java/org/hcrival/api/profile/grant/GrantService.java
new file mode 100644
index 0000000..ad41a52
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/grant/GrantService.java
@@ -0,0 +1,91 @@
+package org.hcrival.api.profile.grant;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.util.exception.DataNotFoundException;
+
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+public class GrantService {
+
+ private final InvictusAPI api;
+
+ @Getter
+ private final LoadingCache cache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public Grant load(UUID id) throws DataNotFoundException {
+ Document document = api.getMongoService().getGrants()
+ .find(Filters.eq("id", id.toString())).first();
+ if (document == null)
+ throw new DataNotFoundException();
+
+ return new Grant(document);
+ }
+ });
+
+ @Getter
+ private final LoadingCache> playerCache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader>() {
+ @Override
+ public List load(UUID uuid) {
+ List grants = new ArrayList<>();
+ api.getMongoService().getGrants().find(Filters.eq("uuid", uuid.toString()))
+ .forEach((Block super Document>) document -> {
+ Grant grant = new Grant(document);
+ if (grant.asRank() == null)
+ return;
+
+ cache.put(grant.getId(), grant);
+ grants.add(grant);
+ });
+
+ return grants;
+ }
+ });
+
+ public Optional getGrant(UUID id) {
+ try {
+ return Optional.ofNullable(cache.get(id));
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public List getGrantsOf(UUID uuid) {
+ try {
+ return playerCache.get(uuid);
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof DataNotFoundException))
+ e.printStackTrace();
+ return Collections.emptyList();
+ }
+ }
+
+ public void saveGrant(Grant grant) {
+ api.getMongoService().getGrants().replaceOne(
+ Filters.eq("id", grant.getId().toString()),
+ grant.toBson(),
+ MongoService.REPLACE_OPTIONS
+ );
+
+ cache.put(grant.getId(), grant);
+ playerCache.refresh(grant.getUuid());
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/grant/StaffController.java b/API/src/main/java/org/hcrival/api/profile/grant/StaffController.java
new file mode 100644
index 0000000..2a6f781
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/grant/StaffController.java
@@ -0,0 +1,70 @@
+package org.hcrival.api.profile.grant;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.rank.Rank;
+import org.hcrival.api.util.UUIDCache;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping
+public class StaffController {
+
+ private static final int STAFF_WEIGHT = 160;
+
+ private final InvictusAPI api;
+
+ @GetMapping(path = "/staffList")
+ public ResponseEntity staffList() {
+ JsonArray array = new JsonArray();
+ for (Rank rank : api.getRankService().getRanks()) {
+ if (rank.getWeight() < STAFF_WEIGHT)
+ continue;
+
+ JsonObject rankObject = new JsonObject();
+ rankObject.addProperty("uuid", rank.getUuid().toString());
+ rankObject.addProperty("name", rank.getName());
+ rankObject.addProperty("weight", rank.getWeight());
+ rankObject.addProperty("webColor", rank.getWebColor());
+
+ JsonArray members = new JsonArray();
+
+ api.getMongoService().getGrants().find(
+ Filters.and(
+ Filters.eq("rank", rank.getUuid().toString()),
+ Filters.eq("removed", false)
+ )
+ ).forEach((Block super Document>) document -> {
+ Grant grant = new Grant(document);
+ if (grant.isRemoved() || !grant.isActive())
+ return;
+
+ JsonObject member = new JsonObject();
+ member.addProperty("uuid", grant.getUuid().toString());
+
+ String name = UUIDCache.getName(grant.getUuid());
+ member.addProperty("name", name == null ? "N/A" : name);
+
+ members.add(member);
+ });
+
+ rankObject.add("members", members);
+ array.add(rankObject);
+ }
+
+ return new ResponseEntity<>(array, HttpStatus.OK);
+ }
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/grant/rpacket/GrantAddPacket.java b/API/src/main/java/org/hcrival/api/profile/grant/rpacket/GrantAddPacket.java
new file mode 100644
index 0000000..71e951f
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/grant/rpacket/GrantAddPacket.java
@@ -0,0 +1,26 @@
+package org.hcrival.api.profile.grant.rpacket;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import org.hcrival.api.redis.packet.RPacket;
+
+import java.util.UUID;
+
+@NoArgsConstructor
+@AllArgsConstructor
+public class GrantAddPacket implements RPacket {
+
+ private UUID uuid;
+ private UUID rankUuid;
+ private long duration;
+
+ @Override
+ public void receive() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "cc.invictusgames.invictus.grant.packets.GrantAddPacket";
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/grant/rpacket/GrantRemovePacket.java b/API/src/main/java/org/hcrival/api/profile/grant/rpacket/GrantRemovePacket.java
new file mode 100644
index 0000000..af4aebb
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/grant/rpacket/GrantRemovePacket.java
@@ -0,0 +1,25 @@
+package org.hcrival.api.profile.grant.rpacket;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import org.hcrival.api.redis.packet.RPacket;
+
+import java.util.UUID;
+
+@NoArgsConstructor
+@AllArgsConstructor
+public class GrantRemovePacket implements RPacket {
+
+ private UUID uuid;
+ private UUID rankUuid;
+
+ @Override
+ public void receive() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "cc.invictusgames.invictus.grant.packets.GrantRemovePacket";
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/note/Note.java b/API/src/main/java/org/hcrival/api/profile/note/Note.java
new file mode 100644
index 0000000..9d98485
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/note/Note.java
@@ -0,0 +1,47 @@
+package org.hcrival.api.profile.note;
+
+import com.google.gson.JsonObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.util.configuration.JsonConfigurationService;
+
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+public class Note {
+
+ private UUID id;
+ private UUID uuid;
+ private String addedBy;
+ private String note;
+ private String addedOn;
+ private long addedAt;
+
+ public Note(Document document) {
+ this.id = UUID.fromString(document.getString("id"));
+ this.uuid = UUID.fromString(document.getString("uuid"));
+ this.addedBy = document.getString("addedBy");
+ this.note = document.getString("note");
+ this.addedOn = document.getString("addedOn");
+ this.addedAt = document.get("addedAt", Number.class).longValue();
+ }
+
+ public Document toBson() {
+ Document document = new Document();
+ document.append("id", id.toString());
+ document.append("uuid", uuid.toString());
+ document.append("addedBy", addedBy);
+ document.append("note", note);
+ document.append("addedOn", addedOn);
+ document.append("addedAt", addedAt);
+ return document;
+ }
+
+ public JsonObject toJson() {
+ return JsonConfigurationService.gson.toJsonTree(this).getAsJsonObject();
+ }
+
+
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/note/NoteController.java b/API/src/main/java/org/hcrival/api/profile/note/NoteController.java
new file mode 100644
index 0000000..cdcefb3
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/note/NoteController.java
@@ -0,0 +1,100 @@
+package org.hcrival.api.profile.note;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.util.JsonBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Optional;
+import java.util.UUID;
+
+@RestController
+@RequestMapping(path = "/profile/{uuid}/notes")
+@Controller(value = "/profile/{uuid}/notes")
+public class NoteController {
+
+ private final InvictusAPI api;
+ private final NoteService noteService;
+
+ public NoteController(InvictusAPI api) {
+ this.api = api;
+ this.noteService = api.getNoteService();
+ }
+
+ @PostMapping()
+ public ResponseEntity addNote(@RequestBody JsonObject body, @PathVariable(name = "uuid") UUID uuid) {
+ JsonBuilder response = new JsonBuilder();
+ UUID id = body.has("id")
+ ? UUID.fromString(body.get("id").getAsString())
+ : UUID.randomUUID();
+
+ if (noteService.getNote(id).isPresent()) {
+ response.add("message", "Note already exists");
+ return new ResponseEntity<>(response.build(), HttpStatus.CONFLICT);
+ }
+
+ Note note = new Note();
+ note.setId(id);
+ note.setUuid(uuid);
+ note.setAddedBy(body.get("addedBy").getAsString());
+ note.setNote(body.get("note").getAsString());
+ note.setAddedOn(body.has("addedOn")
+ ? body.get("addedOn").getAsString()
+ : "Website");
+ note.setAddedAt(body.has("addedAt")
+ ? body.get("addedAt").getAsLong()
+ : System.currentTimeMillis());
+
+ noteService.saveNote(note);
+ return new ResponseEntity<>(note.toJson(), HttpStatus.CREATED);
+ }
+
+ @GetMapping
+ public ResponseEntity getNotesOf(@PathVariable(name = "uuid") UUID uuid) {
+ JsonArray notes = new JsonArray();
+ noteService.getNotesOf(uuid).forEach(note -> notes.add(note.toJson()));
+ return new ResponseEntity<>(notes, HttpStatus.OK);
+ }
+
+ @PutMapping(path = "/{id}")
+ public ResponseEntity updateNote(@RequestBody JsonObject body,
+ @PathVariable(name = "uuid") UUID uuid,
+ @PathVariable(name = "id") UUID id) {
+ JsonBuilder response = new JsonBuilder();
+ Optional noteOpt = noteService.getNote(id);
+
+ if (!noteOpt.isPresent()) {
+ response.add("message", "Note not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Note note = noteOpt.get();
+
+ if (body.has("note"))
+ note.setNote(body.get("note").getAsString());
+
+ noteService.saveNote(note);
+ return new ResponseEntity<>(note.toJson(), HttpStatus.OK);
+ }
+
+ @DeleteMapping(path = "/{id}")
+ public ResponseEntity removeNote(@PathVariable(name = "uuid") UUID uuid,
+ @PathVariable(name = "id") UUID id) {
+ JsonBuilder response = new JsonBuilder();
+ Optional noteOpt = noteService.getNote(id);
+
+ if (!noteOpt.isPresent()) {
+ response.add("message", "Note not found");
+ return new ResponseEntity<>(response.build(), HttpStatus.NOT_FOUND);
+ }
+
+ Note note = noteOpt.get();
+ noteService.deleteNote(note);
+ return new ResponseEntity<>(note.toJson(), HttpStatus.OK);
+ }
+}
diff --git a/API/src/main/java/org/hcrival/api/profile/note/NoteService.java b/API/src/main/java/org/hcrival/api/profile/note/NoteService.java
new file mode 100644
index 0000000..80e5187
--- /dev/null
+++ b/API/src/main/java/org/hcrival/api/profile/note/NoteService.java
@@ -0,0 +1,94 @@
+package org.hcrival.api.profile.note;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.mongodb.Block;
+import com.mongodb.client.model.Filters;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bson.Document;
+import org.hcrival.api.InvictusAPI;
+import org.hcrival.api.mongo.MongoService;
+import org.hcrival.api.util.exception.DataNotFoundException;
+
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RequiredArgsConstructor
+public class NoteService {
+
+ private final InvictusAPI api;
+
+ @Getter
+ private final LoadingCache cache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader() {
+ @Override
+ public Note load(UUID id) throws DataNotFoundException {
+ Document document = api.getMongoService().getNotes()
+ .find(Filters.eq("id", id.toString())).first();
+ if (document == null)
+ throw new DataNotFoundException();
+
+ return new Note(document);
+ }
+ });
+
+ @Getter
+ private final LoadingCache> playerCache = CacheBuilder.newBuilder()
+ .expireAfterAccess(15L, TimeUnit.MINUTES)
+ .build(new CacheLoader>() {
+ @Override
+ public List