diff --git a/.gitignore b/.gitignore index b7a97db..8c3a071 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.iml target .secret +dependency-reduced-pom.xml \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4408a99 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/main/resources/AcroBot"] + path = src/main/resources/AcroBot + url = https://github.com/theacrobot/AcroBot.git diff --git a/pom.xml b/pom.xml index b9b9c83..1d1948f 100644 --- a/pom.xml +++ b/pom.xml @@ -12,10 +12,11 @@ UTF-8 1.8 1.8 - 2.9.10.3 + 2.9.10.4 1.25.0 1.70.0 4.3.6.Final + 1.26 @@ -38,6 +39,12 @@ ${jackson.version} + + org.yaml + snakeyaml + ${snakeyaml.version} + + diff --git a/sec b/sec new file mode 100644 index 0000000..d840a65 Binary files /dev/null and b/sec differ diff --git a/src/main/java/com/redhat/constants/Constants.java b/src/main/java/com/redhat/constants/Constants.java index b55d149..cc7bbff 100644 --- a/src/main/java/com/redhat/constants/Constants.java +++ b/src/main/java/com/redhat/constants/Constants.java @@ -4,9 +4,8 @@ public class Constants { public static final String CREDENTIALS_PATH_ENV_PROPERTY = "GOOGLE_APPLICATION_CREDENTIALS"; public static final String PROJECT_ID = System.getenv("PROJECT_ID"); public static final String SUBSCRIPTION_ID = System.getenv("SUBSCRIPTION_ID"); - public static final String SUDO_PASSWORD = System.getenv("SUDO_PASSWORD"); public static final String HANGOUTS_CHAT_API_SCOPE = "https://www.googleapis.com/auth/chat.bot"; - + public static final String YAML_SOURCE = "AcroBot/data/abbrev.yaml"; // ClassLoader path // Response templates public static final String RESPONSE_URL_TEMPLATE = "https://chat.googleapis.com/v1/__SPACE_ID__/messages"; public static final String ADDED_RESPONSE = "Thank you for adding me! Send `@Acrobot help` for more information about me."; @@ -30,5 +29,19 @@ public class Constants { "All of the actions work in a direct message without tagging `@Acrobot`. Whitespaces shouldn't matter," + "and you can input acronym in both lower- and uppercase; it will be matched regardless of the capitalisation. \n\n" + "Acrobot is implemented by Marek Czernek. You can find documentation and file issues or suggest improvements at " + - "https://github.com/m-czernek/acrobot"; + "https://github.com/m-czernek/acrobot \n\n" + + "Currently, Acrobot is testing injecting IRC acronym database into the results. For feedback, comments, or suggestions "+ + "about the function, visit https://github.com/m-czernek/acrobot/pull/12"; + + public static final String FOUND_YAML_TEXT = "\n\n_Found requested acronym in the IRC database. " + + "Send @Acrobot help for more information about this feature._\n"; + + // we should not return null for sudo password + private static final String SUDO_PASSWORD = System.getenv("SUDO_PASSWORD"); + public static String getSudoPassword(String defaultValue) { + if(SUDO_PASSWORD == null) { + return defaultValue; + } + return SUDO_PASSWORD; + } } diff --git a/src/main/java/com/redhat/constants/MessageType.java b/src/main/java/com/redhat/constants/MessageType.java new file mode 100644 index 0000000..98544a8 --- /dev/null +++ b/src/main/java/com/redhat/constants/MessageType.java @@ -0,0 +1,17 @@ +package com.redhat.constants; + +public enum MessageType { + ADDED_TO_ROOM(Constants.ADDED_RESPONSE), + SUDO_RESPONSE("SUDO_PASSWORD"), + INVALID_MESSAGE(Constants.INCORRECT_FORMAT_FOR_SAVING_ACRONYM), + HELP(Constants.HELP_TEXT), + UPDATE_OR_REMOVE("UPDATE_REMOVE"), + SAVE_OR_MERGE("SAVE_MERGE"), + GET_ACRONYM("GET_ACRONYM"); + + public final String eventType; + + private MessageType(String eventType) { + this.eventType = eventType; + } +} diff --git a/src/main/java/com/redhat/messages/AcroBot.java b/src/main/java/com/redhat/messages/AcroBot.java index c9a9ef5..60efebb 100644 --- a/src/main/java/com/redhat/messages/AcroBot.java +++ b/src/main/java/com/redhat/messages/AcroBot.java @@ -16,6 +16,7 @@ import com.google.cloud.pubsub.v1.MessageReceiver; import com.google.pubsub.v1.PubsubMessage; import com.redhat.constants.Constants; +import com.redhat.constants.MessageType; import java.io.FileInputStream; import java.util.Collections; @@ -27,6 +28,7 @@ public class AcroBot implements MessageReceiver { private HttpTransport httpTransport; private HttpRequestFactory requestFactory; private MessageHelper helper; + private YamlAcrobotInjector yamlAcrobotInjector; public AcroBot() throws Exception { credential = GoogleCredential @@ -35,6 +37,7 @@ public AcroBot() throws Exception { httpTransport = GoogleNetHttpTransport.newTrustedTransport(); requestFactory = httpTransport.createRequestFactory(credential); helper = new MessageHelper(); + yamlAcrobotInjector = new YamlAcrobotInjector(); } // Called when a message is received by the subscriber. @@ -68,7 +71,13 @@ public void handle(JsonNode eventJson) throws Exception { break; } case "MESSAGE": - responseNode.put("text", helper.handleMessageAction(eventJson)); + String response = helper.handleMessageAction(eventJson); + + if(MessageTypeHelper.determineMessageAction(eventJson) == MessageType.GET_ACRONYM) { + response = yamlAcrobotInjector.injectYamlAcronyms(eventJson, response); + } + + responseNode.put("text", response); // In case of message, post the response in the same thread. ObjectNode threadNode = jsonNodeFactory.objectNode(); diff --git a/src/main/java/com/redhat/messages/MessageHelper.java b/src/main/java/com/redhat/messages/MessageHelper.java index 49b74b0..312c586 100644 --- a/src/main/java/com/redhat/messages/MessageHelper.java +++ b/src/main/java/com/redhat/messages/MessageHelper.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.redhat.constants.Constants; +import com.redhat.constants.MessageType; import com.redhat.entities.Acronym; import com.redhat.entities.Explanation; import com.redhat.persistence.AcronymExplanationDal; @@ -15,41 +16,29 @@ public class MessageHelper { private AcronymExplanationDal acronymExplanationDal = new AcronymExplanationDal(); public String handleMessageAction(JsonNode eventJson) { - String resp; - String message; String authorEmail = eventJson.get("user").get("email").asText(); + MessageType type = MessageTypeHelper.determineMessageAction(eventJson); - try { - message = eventJson.get("message").get("argumentText").asText().trim(); - } catch (NullPointerException e) { - // Acrobot was added via mention to a room, and has no argument text - return Constants.ADDED_RESPONSE; - } - - if(message.startsWith(Constants.SUDO_PASSWORD)) { - return AdministrativeMessageHelper.handleAdminMessage(message); + if(type == MessageType.ADDED_TO_ROOM) { + return type.eventType; } - if(message.startsWith("!")) { - message = message.substring(1); - - // Validate message - if (!isMessageValid(message)) { - return Constants.INCORRECT_FORMAT_FOR_SAVING_ACRONYM; - } - - if(message.contains("=>")) { - resp = updateOrRemoveExplanation(message, authorEmail); - } else { - resp = saveOrMergeAcronym(message, authorEmail); - } - - } else if (message.equals("help")) { - resp = Constants.HELP_TEXT; - } else { - resp = getAcronymAsString(message); + String message = eventJson.get("message").get("argumentText").asText().trim(); + + switch (type){ + case SUDO_RESPONSE: + return AdministrativeMessageHelper.handleAdminMessage(message); + case UPDATE_OR_REMOVE: + // Message starts with "!" + return updateOrRemoveExplanation(message.substring(1), authorEmail); + case SAVE_OR_MERGE: + // Message starts with "!" + return saveOrMergeAcronym(message.substring(1), authorEmail); + case GET_ACRONYM: + return getAcronymAsString(message); + default: + return type.eventType; } - return resp; } private String updateOrRemoveExplanation(String message, String authorEmail) { @@ -102,10 +91,6 @@ private String saveOrMergeAcronym(String message, String authorEmail) { return resp; } - private boolean isMessageValid(String message) { - return message.contains("=") && (!message.trim().endsWith("=")); - } - private String[] splitMessageToSaveAndTrim(String message) { return trimArray(message.split("=", 2)); } diff --git a/src/main/java/com/redhat/messages/MessageTypeHelper.java b/src/main/java/com/redhat/messages/MessageTypeHelper.java new file mode 100644 index 0000000..d17cc68 --- /dev/null +++ b/src/main/java/com/redhat/messages/MessageTypeHelper.java @@ -0,0 +1,48 @@ +package com.redhat.messages; + +import com.fasterxml.jackson.databind.JsonNode; +import com.redhat.constants.Constants; +import com.redhat.constants.MessageType; + +public class MessageTypeHelper { + + public static MessageType determineMessageAction(JsonNode eventJson) { + final JsonNode msgNode = eventJson.get("message").get("argumentText"); + + if(msgNode == null) { + // Acrobot was added via mention to a room, and has no argument text + return MessageType.ADDED_TO_ROOM; + } + + String message = msgNode.asText(); + + if(message.startsWith(Constants.getSudoPassword("PW does not exist"))) { + return MessageType.SUDO_RESPONSE; + } + + if(message.startsWith("!")) { + message = message.substring(1); + + // Validate message + if (!isMessageValid(message)) { + return MessageType.INVALID_MESSAGE; + } + + if(message.contains("=>")) { + return MessageType.UPDATE_OR_REMOVE; + } else { + return MessageType.SAVE_OR_MERGE; + } + } + + if (message.equals("help")) { + return MessageType.HELP; + } else { + return MessageType.GET_ACRONYM; + } + } + + private static boolean isMessageValid(String message) { + return message.contains("=") && (!message.trim().endsWith("=")); + } +} diff --git a/src/main/java/com/redhat/messages/YamlAcrobotInjector.java b/src/main/java/com/redhat/messages/YamlAcrobotInjector.java new file mode 100644 index 0000000..36a362a --- /dev/null +++ b/src/main/java/com/redhat/messages/YamlAcrobotInjector.java @@ -0,0 +1,32 @@ +package com.redhat.messages; + +import com.fasterxml.jackson.databind.JsonNode; +import com.redhat.constants.Constants; +import com.redhat.entities.Acronym; +import com.redhat.entities.Explanation; +import com.redhat.persistence.YamlDal; + +import java.util.Set; + +public class YamlAcrobotInjector { + private YamlDal dal = new YamlDal(); + + public String injectYamlAcronyms(JsonNode eventJson, String originalMessage) { + String res = Constants.FOUND_YAML_TEXT; + String requestedAcronym = eventJson.get("message").get("argumentText").asText().trim(); + Set yamlAcronyms = dal.getAcronymsByName(requestedAcronym); + + if(yamlAcronyms.isEmpty()) { + return originalMessage; + } + + + for(Acronym a : yamlAcronyms) { + for(Explanation e : a.getExplanations()) { + res += e.getExplanation() + "\n"; + } + } + + return originalMessage + res; + } +} diff --git a/src/main/java/com/redhat/persistence/YamlDal.java b/src/main/java/com/redhat/persistence/YamlDal.java new file mode 100644 index 0000000..f4bbc2c --- /dev/null +++ b/src/main/java/com/redhat/persistence/YamlDal.java @@ -0,0 +1,56 @@ +package com.redhat.persistence; + +import com.redhat.constants.Constants; +import com.redhat.entities.Acronym; +import com.redhat.entities.Explanation; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; +import org.yaml.snakeyaml.resolver.Resolver; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class YamlDal { + private Map> yamlDatabase; + + + public YamlDal() { + InputStream is = this.getClass() + .getClassLoader() + .getResourceAsStream(Constants.YAML_SOURCE); + this.yamlDatabase = new Yaml(new Constructor(), new Representer(), + new DumperOptions(), new CustomResolver()) + .load(is); + } + + public Set getAcronymsByName(String name) { + Set res = new HashSet<>(); + for(String key : yamlDatabase.keySet()) { + Map databaseSection = yamlDatabase.get(key); + if(databaseSection.containsKey(name)) { + Acronym a = new Acronym(name); + Set e = new HashSet<>(); + e.add(new Explanation(databaseSection.get(name))); + a.setExplanations(e); + res.add(a); + } + } + return res; + } + + // Custom resolver such that we don't have implicit number -> Integer or + // number -> Float typing. We want String or null values only. + private class CustomResolver extends Resolver { + protected void addImplicitResolvers() { + addImplicitResolver(Tag.MERGE, MERGE, "<"); + addImplicitResolver(Tag.NULL, EMPTY, null); + } + + } +} + diff --git a/src/main/resources/AcroBot b/src/main/resources/AcroBot new file mode 160000 index 0000000..3eb87d2 --- /dev/null +++ b/src/main/resources/AcroBot @@ -0,0 +1 @@ +Subproject commit 3eb87d2319f535c6195cd7b9dd061926e1fef2cd diff --git a/src/test/java/com/redhat/YmlLayerTest.java b/src/test/java/com/redhat/YmlLayerTest.java new file mode 100644 index 0000000..fc50d46 --- /dev/null +++ b/src/test/java/com/redhat/YmlLayerTest.java @@ -0,0 +1,34 @@ +package com.redhat; + +import com.redhat.persistence.YamlDal; +import org.assertj.core.api.Assertions; +import org.junit.Test; + + +public class YmlLayerTest { + private YamlDal dal = new YamlDal(); + // Corner cases + // Acro is a number + private static String numberAcro = "82576"; + private static String numberAcroExplanation = "Intel 82576 Gigabit Ethernet Controller"; + // Acro has starts with a lowercase char + private static String lowerCaseAcro = "dNAT"; + private static String lowerCaseAcroExplanation = "Dynamic Network Address Translation @networking"; + // Acro contains special chars + private static String specialCharAcro = "SMI-S"; + private static String specialCharAcroExplanation = "Storage Management Initiative Specification"; + + @Test + public void testCornerCases() { + Assertions.assertThat(dal.getAcronymsByName(numberAcro)) + .hasSize(1) + .filteredOn(acronym -> acronym.getExplanations().contains(numberAcroExplanation)); + Assertions.assertThat(dal.getAcronymsByName(lowerCaseAcro)) + .hasSize(1) + .filteredOn(acronym -> acronym.getExplanations().contains(lowerCaseAcroExplanation)); + Assertions.assertThat(dal.getAcronymsByName(specialCharAcro)) + .hasSize(1) + .filteredOn(acronym -> acronym.getExplanations().contains(specialCharAcroExplanation)); + Assertions.assertThat(dal.getAcronymsByName("NON_EXISTENT")).isEmpty(); + } +}