diff --git a/api/src/main/java/zone/themcgamer/api/API.java b/api/src/main/java/zone/themcgamer/api/API.java index b01a4fc..5605dcc 100644 --- a/api/src/main/java/zone/themcgamer/api/API.java +++ b/api/src/main/java/zone/themcgamer/api/API.java @@ -11,6 +11,7 @@ import zone.themcgamer.api.model.ModelSerializer; import zone.themcgamer.api.model.impl.*; import zone.themcgamer.api.repository.AccountRepository; import zone.themcgamer.api.route.AccountRoute; +import zone.themcgamer.api.route.ChatRoute; import zone.themcgamer.api.route.PlayerStatusRoute; import zone.themcgamer.api.route.ServersRoute; import zone.themcgamer.data.APIAccessLevel; @@ -63,6 +64,7 @@ public class API { addRoute(new ServersRoute()); addRoute(new AccountRoute(accountRepository)); addRoute(new PlayerStatusRoute()); + addRoute(new ChatRoute()); // 404 Handling Spark.notFound((request, response) -> { diff --git a/api/src/main/java/zone/themcgamer/api/route/ChatRoute.java b/api/src/main/java/zone/themcgamer/api/route/ChatRoute.java new file mode 100644 index 0000000..2d7797b --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/route/ChatRoute.java @@ -0,0 +1,70 @@ +package zone.themcgamer.api.route; + +import com.google.common.base.Strings; +import spark.Request; +import spark.Response; +import zone.themcgamer.api.APIException; +import zone.themcgamer.api.APIVersion; +import zone.themcgamer.api.RestPath; +import zone.themcgamer.common.EnumUtils; +import zone.themcgamer.data.APIAccessLevel; +import zone.themcgamer.data.ChatFilterLevel; +import zone.themcgamer.data.jedis.data.APIKey; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * This route handles everything associated with the chat + * + * @author Braydon + */ +public class ChatRoute { + private static final Map> filteredWords = new HashMap<>() {{ + put(ChatFilterLevel.LOW, Arrays.asList( + "ass" + )); + put(ChatFilterLevel.MEDIUM, Arrays.asList( + "fuck", "shit" + )); + put(ChatFilterLevel.HIGH, Arrays.asList( + "nigger" + )); + }}; + + /** + * This path handles filtering the provided text with the provided {@link ChatFilterLevel} + */ + @RestPath(path = "/filter/:text/:level", version = APIVersion.V1, accessLevel = APIAccessLevel.DEV) + public String filterText(Request request, Response response, APIKey apiKey) throws APIException { + String text = request.params(":text"); + ChatFilterLevel chatFilterLevel = EnumUtils.fromString(ChatFilterLevel.class, request.params(":level").toUpperCase()); + if (chatFilterLevel == null) + throw new APIException("Invalid filter level, valid options: " + Arrays.stream(ChatFilterLevel.values()).map(ChatFilterLevel::name).collect(Collectors.joining(", "))); + String filteredMessage = text; + text = text.replace("{dot}", "."); + if (chatFilterLevel.isUrls()) { + // TODO: 2/20/21 filter urls with regex patterns + } + if (chatFilterLevel.isIps()) { + // TODO: 2/20/21 filter ips with regex patterns + } + List filteredWords = new ArrayList<>(); + for (ChatFilterLevel filterLevel : ChatFilterLevel.values()) { + if (chatFilterLevel.ordinal() >= filterLevel.ordinal()) { + List list = ChatRoute.filteredWords.getOrDefault(filterLevel, new ArrayList<>()); + if (list.isEmpty()) + continue; + filteredWords.addAll(list); + } + } + for (String word : text.split(" ")) { + for (String filteredWord : filteredWords) { + if (word.toLowerCase().contains(filteredWord.toLowerCase())) { + filteredMessage = filteredMessage.replace(word, Strings.repeat("*", word.length())); + } + } + } + return filteredMessage; + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c6b8271..4eb08f3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,7 +41,7 @@ subprojects { compileOnly("org.slf4j:slf4j-simple:1.7.30") - implementation("com.google.code.gson:gson:2.7") + implementation("com.google.code.gson:gson:2.8.5") implementation("commons-io:commons-io:2.6") diff --git a/core/src/main/java/zone/themcgamer/core/api/WebAPI.java b/core/src/main/java/zone/themcgamer/core/api/WebAPI.java new file mode 100644 index 0000000..5af4df2 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/api/WebAPI.java @@ -0,0 +1,37 @@ +package zone.themcgamer.core.api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.NonNull; +import zone.themcgamer.core.api.json.JsonRequest; +import zone.themcgamer.core.api.json.JsonResponse; +import zone.themcgamer.data.ChatFilterLevel; + +import java.net.http.HttpClient; +import java.time.Duration; + +/** + * @author Braydon + */ +public class WebAPI { + public static final String API_KEY = "406321cc-48f3-454b-900a-972fd88e4882"; + private static final String URL = "https://api.mcgamerzone.net/v1"; + public static HttpClient HTTP_CLIENT; + + static { + HTTP_CLIENT = java.net.http.HttpClient.newBuilder() + .version(java.net.http.HttpClient.Version.HTTP_1_1) + .followRedirects(java.net.http.HttpClient.Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(30L)) + .build(); + } + + public static String filterText(@NonNull String text, @NonNull ChatFilterLevel chatFilterLevel) { + JsonResponse jsonResponse = new JsonRequest(URL + "/filter/" + text.replaceAll(" ", "%20") + "/" + chatFilterLevel.name()).getResponse(); + JsonElement jsonElement = jsonResponse.getJsonElement(); + if (!(jsonElement instanceof JsonObject)) + return text; + JsonObject jsonObject = (JsonObject) jsonElement; + return jsonObject.get("value").getAsString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/api/json/JsonRequest.java b/core/src/main/java/zone/themcgamer/core/api/json/JsonRequest.java new file mode 100644 index 0000000..93544ef --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/api/json/JsonRequest.java @@ -0,0 +1,64 @@ +package zone.themcgamer.core.api.json; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import zone.themcgamer.core.api.WebAPI; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +@AllArgsConstructor @Slf4j(topic = "Json Request") +public class JsonRequest { + private final String url; + + /** + * Get the {@link JsonResponse} from the provided url + * + * @return the response + */ + public JsonResponse getResponse() { + try { + return checkResponse(WebAPI.HTTP_CLIENT.send(buildRequest(), HttpResponse.BodyHandlers.ofString())); + } catch (IOException | InterruptedException ex) { + ex.printStackTrace(); + } + return null; + } + + /** + * Get the {@link JsonResponse} from the provided url asynchronously + * + * @param consumer the consumer the response will be accepted in + */ + public void getResponseAsync(Consumer consumer) { + WebAPI.HTTP_CLIENT.sendAsync(buildRequest(), HttpResponse.BodyHandlers.ofString()) + .thenAccept(httpResponse -> consumer.accept(checkResponse(httpResponse))); + } + + private JsonResponse checkResponse(HttpResponse httpResponse) { + int statusCode = httpResponse.statusCode(); + if (statusCode != 200) { + log.warn("Response from \"" + url + "\" returned status code " + statusCode + ":"); + log.warn("Body: " + httpResponse.body()); + log.warn("Headers: " + httpResponse.headers().toString()); + } + return new JsonResponse(httpResponse); + } + + private HttpRequest buildRequest() { + return HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .timeout(Duration.ofMinutes(1L)) + .header("Content-Type", "application/json") + .header("key", WebAPI.API_KEY) + .build(); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/api/json/JsonResponse.java b/core/src/main/java/zone/themcgamer/core/api/json/JsonResponse.java new file mode 100644 index 0000000..6270278 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/api/json/JsonResponse.java @@ -0,0 +1,25 @@ +package zone.themcgamer.core.api.json; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import lombok.Getter; + +import java.net.http.HttpResponse; + +/** + * This class represents a response from a {@link JsonRequest} + * + * @author Braydon + */ +@Getter +public class JsonResponse { + private final HttpResponse httpResponse; + private final String json; + private final JsonElement jsonElement; + + public JsonResponse(HttpResponse httpResponse) { + this.httpResponse = httpResponse; + json = httpResponse.body(); + jsonElement = new JsonParser().parse(json); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java b/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java index 9ba7ef1..e351017 100644 --- a/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java +++ b/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java @@ -13,6 +13,7 @@ import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.plugin.java.JavaPlugin; import zone.themcgamer.core.account.Account; import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.api.WebAPI; import zone.themcgamer.core.badSportSystem.BadSportClient; import zone.themcgamer.core.badSportSystem.BadSportSystem; import zone.themcgamer.core.badSportSystem.Punishment; @@ -26,6 +27,7 @@ import zone.themcgamer.core.common.Style; import zone.themcgamer.core.cooldown.CooldownHandler; import zone.themcgamer.core.module.Module; import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.data.ChatFilterLevel; import zone.themcgamer.data.Rank; import java.util.*; @@ -41,19 +43,21 @@ public class ChatManager extends Module { @Getter private final Map emotes = new HashMap<>(); { - emotes.put("¯\\_(ツ)_/¯", ":shrug:"); - emotes.put("⊂(´・◡・⊂ )∘˚˳°", ":happyghost:"); - emotes.put("ヽ༼ຈل͜ຈ༽ノ", ":donger:"); - emotes.put("ಠ_ಠ", ":disapproval:"); - emotes.put("(≖_≖)", ":squint:"); - emotes.put("(౮⦦ʖ౮)", ":lenny:"); - emotes.put("┬─┬ ノ( ゜-゜ノ)", ":unflip:"); - emotes.put("(☞゚ヮ゚)☞", ":same:"); - emotes.put("ლ(ಥ Д ಥ )ლ", ":why:"); - emotes.put("(╯°□°)╯︵ ┻━┻", ":tableflip:"); - emotes.put("⊂(•̀_•́⊂ )∘˚˳°", ":angryghost:"); - emotes.put("( ˘ ³˘)♥", ":kiss:"); - emotes.put("༼ つ ◕_◕ ༽つ", ":ameno:"); + emotes.put(":shrug:", "¯\\_(ツ)_/¯"); + emotes.put(":happyghost:", "⊂(´・◡・⊂ )∘˚˳°"); + emotes.put(":donger:", "ヽ༼ຈل͜ຈ༽ノ"); + emotes.put(":disapproval:", "ಠ_ಠ"); + emotes.put(":squint:", "(≖_≖)"); + emotes.put(":lenny:", "(౮⦦ʖ౮)"); + emotes.put(":unflip:", "┬─┬ ノ( ゜-゜ノ)"); + emotes.put(":same:", "(☞゚ヮ゚)☞"); + emotes.put(":why:", "ლ(ಥ Д ಥ )ლ"); + emotes.put(":tableflip:", "(╯°□°)╯︵ ┻━┻"); + emotes.put(":angryghost:", "⊂(•̀_•́⊂ )∘˚˳°"); + emotes.put(":kiss:", "( ˘ ³˘)♥"); + emotes.put(":ameno:", "༼ つ ◕_◕ ༽つ"); + emotes.put(":heart:", "❤"); + emotes.put("<3", "❤"); } public ChatManager(JavaPlugin plugin, AccountManager accountManager, BadSportSystem badSportSystem, IChatComponent[] chatComponents) { @@ -88,7 +92,11 @@ public class ChatManager extends Module { player.sendMessage(Style.error("Bad Sport", PunishmentCategory.format(optionalMute.get()))); return; } - // TODO: 1/26/21 filter message + try { + message = WebAPI.filterText(message, ChatFilterLevel.HIGH); + } catch (Exception ex) { + player.sendMessage(Style.error("Chat", "§cProblem filtering chat message: §f" + ex.getLocalizedMessage())); + } if (message.trim().isEmpty()) { player.sendMessage(Style.error("Chat", "§cCannot send empty chat message")); return; @@ -104,7 +112,7 @@ public class ChatManager extends Module { return; } for (Map.Entry emote : emotes.entrySet()) - message = message.replace(emote.getValue(), emote.getKey()); + message = message.replace(emote.getKey(), emote.getValue()); List components = new ArrayList<>(); for (IChatComponent chatComponent : chatComponents) { BaseComponent component = chatComponent.getComponent(player); diff --git a/core/src/main/java/zone/themcgamer/core/chat/command/EmotesCommand.java b/core/src/main/java/zone/themcgamer/core/chat/command/EmotesCommand.java index 0809ae8..547e145 100644 --- a/core/src/main/java/zone/themcgamer/core/chat/command/EmotesCommand.java +++ b/core/src/main/java/zone/themcgamer/core/chat/command/EmotesCommand.java @@ -21,6 +21,6 @@ public class EmotesCommand { Player player = command.getPlayer(); player.sendMessage(Style.main("Chat", "Chat Emotes:")); for (Map.Entry entry : chatManager.getEmotes().entrySet()) - player.sendMessage(" §6" + entry.getValue() + " §7-> §b" + entry.getKey()); + player.sendMessage(" §6" + entry.getKey() + " §7-> §b" + entry.getValue()); } } \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/ChatFilterLevel.java b/serverdata/src/main/java/zone/themcgamer/data/ChatFilterLevel.java new file mode 100644 index 0000000..40f9ed6 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/ChatFilterLevel.java @@ -0,0 +1,17 @@ +package zone.themcgamer.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + */ +@AllArgsConstructor +@Getter +public enum ChatFilterLevel { + LOW(false, true), + MEDIUM(true, true), + HIGH(true, true); + + private final boolean urls, ips; +} \ No newline at end of file