Repository: slackapi/java-slack-sdk
Issue: Event type extract does not match correctly if there are "type" properties in JSON subentries.

The current code for extracting event types seems to assume that the "type:" entry that contains the event type is the first type entry in the JSON that follows "event:". However, this is not always the case. 

E.g. in the following message `{"authed_users":["RETRACTED"],"event_id":"RETRACTED","api_app_id":"RETRACTED","team_id":"RETRACTED","event":{"client_msg_id":"b5ac4998-4526-4d0b-8bb0-7bd614c1b774","blocks":[{"elements":[{"elements":[{"text":"test","type":"text"}],"type":"rich_text_section"}],"type":"rich_text","block_id":"Ws3Y"}],"event_ts":"1592257234.003200","channel":"RETRACTED","text":"test","team":"RETRACTED","type":"message","channel_type":"channel","user":"RETRACTED","ts":"1592257234.003200"},"type":"event_callback","event_time":1592257234,"token":"RETRACTED"}` the `"type": "text"`value of the "blocks" field comes first. The event for that message is detected as "text" even though it would need to be "message" to arrive in the correct event handler.

https://github.com/slackapi/java-slack-sdk/blob/a2e96f3582189cbd7e9b0bd5a4504bdba4262ce6/slack-app-backend/src/main/java/com/slack/api/app_backend/events/EventTypeExtractorImpl.java#L23

Changed files:
slack-app-backend/src/main/java/com/slack/api/app_backend/events/EventTypeExtractorImpl.java
slack-app-backend/src/test/java/test_locally/app_backend/events/EventTypeDetectorImplTest.java

diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/events/EventTypeExtractorImpl.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/EventTypeExtractorImpl.java
index 10233ad8..d3d586f8 100644
--- a/slack-app-backend/src/main/java/com/slack/api/app_backend/events/EventTypeExtractorImpl.java
+++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/EventTypeExtractorImpl.java
@@ -1,100 +1,39 @@
 package com.slack.api.app_backend.events;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
 public class EventTypeExtractorImpl implements EventTypeExtractor {
 
     @Override
     public String extractEventType(String json) {
-        StringBuilder sb = new StringBuilder();
-        char[] chars = json.toCharArray();
-        boolean isInsideEventData = false;
-        for (int idx = 0; idx < (chars.length - 7); idx++) {
-            if (!isInsideEventData && chars[idx] == '"'
-                    && chars[idx + 1] == 'e'
-                    && chars[idx + 2] == 'v'
-                    && chars[idx + 3] == 'e'
-                    && chars[idx + 4] == 'n'
-                    && chars[idx + 5] == 't'
-                    && chars[idx + 6] == '"'
-                    && chars[idx + 7] == ':') {
-                idx = idx + 8;
-                isInsideEventData = true;
-            }
-
-            if (isInsideEventData && chars[idx] == '"'
-                    && chars[idx + 1] == 't'
-                    && chars[idx + 2] == 'y'
-                    && chars[idx + 3] == 'p'
-                    && chars[idx + 4] == 'e'
-                    && chars[idx + 5] == '"'
-                    && chars[idx + 6] == ':') {
-                idx = idx + 7;
-                int doubleQuoteCount = 0;
-                boolean isPreviousCharEscape = false;
-                while (doubleQuoteCount < 2 && idx < chars.length) {
-                    char c = chars[idx];
-                    if (c == '"' && !isPreviousCharEscape) {
-                        doubleQuoteCount++;
-                    } else {
-                        if (doubleQuoteCount == 1) {
-                            sb.append(c);
-                        }
-                    }
-                    isPreviousCharEscape = c == '\\\\';
-                    idx++;
-                }
-                break;
-            }
-        }
-        return sb.toString();
+        return extractFieldUnderEvent(json, "type");
     }
 
     @Override
     public String extractEventSubtype(String json) {
-        StringBuilder sb = new StringBuilder();
-        char[] chars = json.toCharArray();
-        boolean isInsideEventData = false;
-        for (int idx = 0; idx < (chars.length - 7); idx++) {
-            if (!isInsideEventData && chars[idx] == '"'
-                    && chars[idx + 1] == 'e'
-                    && chars[idx + 2] == 'v'
-                    && chars[idx + 3] == 'e'
-                    && chars[idx + 4] == 'n'
-                    && chars[idx + 5] == 't'
-                    && chars[idx + 6] == '"'
-                    && chars[idx + 7] == ':') {
-                idx = idx + 8;
-                isInsideEventData = true;
-            }
+        return extractFieldUnderEvent(json, "subtype");
+    }
 
-            if (isInsideEventData && chars[idx] == '"'
-                    && chars[idx + 1] == 's'
-                    && chars[idx + 2] == 'u'
-                    && chars[idx + 3] == 'b'
-                    && chars[idx + 4] == 't'
-                    && chars[idx + 5] == 'y'
-                    && chars[idx + 6] == 'p'
-                    && chars[idx + 7] == 'e'
-                    && chars[idx + 8] == '"'
-                    && chars[idx + 9] == ':') {
-                idx = idx + 10;
-                int doubleQuoteCount = 0;
-                boolean isPreviousCharEscape = false;
-                while (doubleQuoteCount < 2 && idx < chars.length) {
-                    char c = chars[idx];
-                    if (c == '"' && !isPreviousCharEscape) {
-                        doubleQuoteCount++;
-                    } else {
-                        if (doubleQuoteCount == 1) {
-                            sb.append(c);
-                        }
+    private static String extractFieldUnderEvent(String json, String fieldName) {
+        try {
+            JsonElement root = JsonParser.parseString(json);
+            if (root != null && root.isJsonObject() && root.getAsJsonObject().has("event")) {
+                JsonElement event = root.getAsJsonObject().get("event");
+                if (event.isJsonObject() && event.getAsJsonObject().has(fieldName)) {
+                    JsonElement eventType = event.getAsJsonObject().get(fieldName);
+                    if (eventType.isJsonPrimitive()) {
+                        return eventType.getAsString();
                     }
-                    isPreviousCharEscape = c == '\\\\';
-                    idx++;
                 }
-                break;
             }
+        } catch (JsonSyntaxException e) {
+            log.debug("Failed to parse {} as a JSON data", json, e);
         }
-        return sb.toString();
+        return "";
     }
 
 }
diff --git a/slack-app-backend/src/test/java/test_locally/app_backend/events/EventTypeDetectorImplTest.java b/slack-app-backend/src/test/java/test_locally/app_backend/events/EventTypeDetectorImplTest.java
index d7f36362..73f21d2e 100644
--- a/slack-app-backend/src/test/java/test_locally/app_backend/events/EventTypeDetectorImplTest.java
+++ b/slack-app-backend/src/test/java/test_locally/app_backend/events/EventTypeDetectorImplTest.java
@@ -11,6 +11,26 @@ public class EventTypeDetectorImplTest {
 
     private EventTypeExtractor eventTypeExtractor = new EventTypeExtractorImpl();
 
+    @Test
+    public void notFound() {
+        {
+            String eventType = eventTypeExtractor.extractEventType("{}");
+            assertThat(eventType, is(""));
+        }
+        {
+            String eventType = eventTypeExtractor.extractEventType("{\\"event\\": {}}");
+            assertThat(eventType, is(""));
+        }
+        {
+            String eventType = eventTypeExtractor.extractEventSubtype("{}");
+            assertThat(eventType, is(""));
+        }
+        {
+            String eventType = eventTypeExtractor.extractEventSubtype("{\\"event\\": {}}");
+            assertThat(eventType, is(""));
+        }
+    }
+
     @Test
     public void detect_goodbye() {
         String payload = "{\\n" +
@@ -54,4 +74,49 @@ public class EventTypeDetectorImplTest {
         assertThat(eventType, is("message"));
     }
 
+    @Test
+    public void issue_489() {
+        String payload = "{\\n" +
+                "  \\"authed_users\\": [\\n" +
+                "    \\"RETRACTED\\"\\n" +
+                "  ],\\n" +
+                "  \\"event_id\\": \\"RETRACTED\\",\\n" +
+                "  \\"api_app_id\\": \\"RETRACTED\\",\\n" +
+                "  \\"team_id\\": \\"RETRACTED\\",\\n" +
+                "  \\"event\\": {\\n" +
+                "    \\"client_msg_id\\": \\"b5ac4998-4526-4d0b-8bb0-7bd614c1b774\\",\\n" +
+                "    \\"blocks\\": [\\n" +
+                "      {\\n" +
+                "        \\"elements\\": [\\n" +
+                "          {\\n" +
+                "            \\"elements\\": [\\n" +
+                "              {\\n" +
+                "                \\"text\\": \\"test\\",\\n" +
+                "                \\"type\\": \\"text\\"\\n" +
+                "              }\\n" +
+                "            ],\\n" +
+                "            \\"type\\": \\"rich_text_section\\"\\n" +
+                "          }\\n" +
+                "        ],\\n" +
+                "        \\"type\\": \\"rich_text\\",\\n" +
+                "        \\"block_id\\": \\"Ws3Y\\"\\n" +
+                "      }\\n" +
+                "    ],\\n" +
+                "    \\"event_ts\\": \\"1592257234.003200\\",\\n" +
+                "    \\"channel\\": \\"RETRACTED\\",\\n" +
+                "    \\"text\\": \\"test\\",\\n" +
+                "    \\"team\\": \\"RETRACTED\\",\\n" +
+                "    \\"type\\": \\"message\\",\\n" +
+                "    \\"channel_type\\": \\"channel\\",\\n" +
+                "    \\"user\\": \\"RETRACTED\\",\\n" +
+                "    \\"ts\\": \\"1592257234.003200\\"\\n" +
+                "  },\\n" +
+                "  \\"type\\": \\"event_callback\\",\\n" +
+                "  \\"event_time\\": 1592257234,\\n" +
+                "  \\"token\\": \\"RETRACTED\\"\\n" +
+                "}";
+        String eventType = eventTypeExtractor.extractEventType(payload);
+        assertThat(eventType, is("message"));
+    }
+
 }